<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8" />
  <title>数据中心布线模拟</title>
  <style>
    body {
      margin: 0;
      font-family: sans-serif;
      background: #1e2130;
      color: #e0e0e0;
    }

    #app {
      display: flex;
      height: 100vh;
    }

    #sidebar {
      width: 300px;
      background: #262b3d;
      padding: 16px;
      box-sizing: border-box;
      overflow-y: auto;
      border-right: 1px solid #3a3f55;
    }

    #three-canvas {
      flex: 1;
      background: #1a1d2a;
      position: relative;
    }

    .input-row {
      margin-bottom: 10px;
    }

    .input-row label {
      display: inline-block;
      width: 80px;
      color: #cfd2e1;
    }

    .input-row input,
    .input-row select {
      background: #323649;
      border: 1px solid #4a4f65;
      color: #e0e0e0;
      padding: 5px;
      border-radius: 3px;
    }

    .cable-info {
      background: #2a2f42;
      padding: 8px;
      border: 1px solid #3a3f55;
      margin-top: 10px;
    }

    .list-item {
      display: flex;
      justify-content: space-between;
      padding: 4px;
      margin: 4px 0;
      background: #323649;
      border-radius: 3px;
    }

    .delete-btn {
      color: #ff6b6b;
      cursor: pointer;
      margin-left: 10px;
    }

    .edit-btn {
      color: #4d94ff;
      cursor: pointer;
      margin-right: 10px;
    }

    .unit {
      color: #9ca0b5;
      font-size: 80%;
    }

    .sub-section {
      margin-left: 15px;
      margin-bottom: 10px;
    }

    #axis-info {
      position: absolute;
      bottom: 10px;
      left: 10px;
      background: rgba(20, 25, 40, 0.7);
      color: white;
      padding: 5px;
      border-radius: 5px;
      font-size: 14px;
    }

    .axis-color {
      display: inline-block;
      width: 12px;
      height: 12px;
      margin-right: 5px;
    }

    .x-color {
      background-color: #ff9900;
    }

    .y-color {
      background-color: #ccff00;
    }

    .z-color {
      background-color: #00aaff;
    }

    #keyboard-controls {
      position: absolute;
      bottom: 10px;
      right: 10px;
      background: rgba(20, 25, 40, 0.7);
      color: white;
      padding: 5px;
      border-radius: 5px;
      font-size: 14px;
    }

    .tabs {
      display: flex;
      margin-bottom: 10px;
    }

    .tab {
      padding: 5px 10px;
      cursor: pointer;
      background: #323649;
      margin-right: 5px;
      border-radius: 3px 3px 0 0;
      color: #9ca0b5;
    }

    .tab.active {
      background: #4d94ff;
      color: #ffffff;
    }

    .view-options {
      background: rgba(20, 25, 40, 0.7);
      color: white;
      padding: 5px 10px;
      border-radius: 5px;
      position: absolute;
      top: 10px;
      right: 10px;
    }

    .toggle-switch {
      display: inline-block;
      vertical-align: middle;
      margin-left: 10px;
    }

    .toggle-switch input[type="checkbox"] {
      display: none;
    }

    .toggle-switch label {
      display: inline-block;
      width: 40px;
      height: 20px;
      background: #323649;
      border-radius: 10px;
      position: relative;
      cursor: pointer;
      transition: 0.3s;
    }

    .toggle-switch label:after {
      content: '';
      position: absolute;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: white;
      top: 2px;
      left: 2px;
      transition: 0.3s;
    }

    .toggle-switch input:checked+label {
      background: #4d94ff;
    }

    .toggle-switch input:checked+label:after {
      left: 22px;
    }

    /* 新增侧边栏样式 - 可折叠部分 */
    .sidebar-section {
      margin-bottom: 10px;
      border: 1px solid #3a3f55;
      border-radius: 5px;
      overflow: hidden;
    }

    .sidebar-header {
      background: #323649;
      padding: 8px 10px;
      cursor: pointer;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-weight: bold;
      user-select: none;
      color: #cfd2e1;
    }

    .sidebar-header:hover {
      background: #3d4258;
    }

    .sidebar-content {
      padding: 10px;
      max-height: 0;
      overflow: hidden;
      transition: max-height 0.3s ease-out;
      background: #262b3d;
    }

    .sidebar-content.active {
      max-height: 500px;
      overflow-y: auto;
    }

    .sidebar-arrow {
      transition: transform 0.3s;
    }

    .sidebar-arrow.active {
      transform: rotate(90deg);
    }

    /* 添加页签系统 */
    .main-tabs {
      display: flex;
      width: 100%;
      margin-bottom: 15px;
      border-bottom: 1px solid #3a3f55;
    }

    .main-tab {
      padding: 8px 5px;
      font-size: 13px;
      cursor: pointer;
      background: #323649;
      border-radius: 5px 5px 0 0;
      margin-right: 2px;
      color: #9ca0b5;
    }

    .main-tab.active {
      background: #4d94ff;
      color: white;
    }

    .tab-content {
      display: none;
    }

    .tab-content.active {
      display: block;
    }

    /* 滚动列表容器 */
    .scroll-list {
      overflow-y: auto;
      border: 1px solid #3a3f55;
      border-radius: 3px;
      padding: 5px;
      margin-bottom: 10px;
      background: #2a2f42;
    }

    /* 控制按钮样式统一 */
    .control-btn {
      background: #4d94ff;
      color: white;
      border: none;
      padding: 6px 10px;
      border-radius: 3px;
      cursor: pointer;
      margin: 5px 3px;
    }

    .control-btn:hover {
      background: #3a7acc;
    }

    /* 计数器标签 */
    .count-badge {
      background: #4d94ff;
      color: white;
      border-radius: 10px;
      padding: 2px 6px;
      font-size: 12px;
      margin-left: 5px;
    }

    /* 编辑表单样式 */
    .edit-form {
      background: #2a2f42;
      border: 1px solid #3a3f55;
      border-radius: 5px;
      padding: 10px;
      margin: 5px 0;
    }

    .edit-form-title {
      font-weight: bold;
      margin-bottom: 8px;
    }

    /* 操作状态通知 */
    .action-notification {
      position: fixed;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(30, 35, 60, 0.9);
      color: white;
      padding: 8px 16px;
      border-radius: 4px;
      font-size: 14px;
      z-index: 1000;
      transition: opacity 0.3s;
      pointer-events: none;
      opacity: 0;
    }

    .action-notification.show {
      opacity: 1;
    }

    /* 机柜属性面板 */
    .cabinet-properties {
      position: absolute;
      top: 20px;
      right: 20px;
      background: rgba(40, 45, 70, 0.9);
      border-radius: 6px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
      width: 280px;
      overflow: hidden;
      z-index: 100;
      border: 1px solid #4a4f65;
    }

    .properties-header {
      padding: 10px 15px;
      background: #4d94ff;
      color: white;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .properties-header h3 {
      margin: 0;
      font-size: 16px;
    }

    .properties-content {
      padding: 15px;
    }

    .prop-row {
      margin-bottom: 12px;
      display: flex;
      align-items: center;
    }

    .prop-row label {
      width: 70px;
      font-weight: bold;
      color: #cfd2e1;
    }

    .prop-row input {
      flex-grow: 1;
      padding: 5px 8px;
      border: 1px solid #4a4f65;
      border-radius: 4px;
      background: #323649;
      color: #e0e0e0;
    }

    .close-btn {
      cursor: pointer;
      font-size: 20px;
      font-weight: bold;
    }

    .danger-btn {
      background-color: #ff6b6b !important;
      margin-left: 10px;
    }

    .danger-btn:hover {
      background-color: #e65a5a !important;
    }

    .info-message {
      padding: 10px;
      margin: 5px 0;
      background-color: #2a3140;
      border-left: 4px solid #4d94ff;
      color: #cfd2e1;
      border-radius: 3px;
      font-size: 14px;
    }

    /* 批量上传相关样式 */
    .batch-upload-section {
      background: #2a3140;
      border: 1px solid #3a3f55;
      border-radius: 5px;
      padding: 10px;
      margin: 10px 0;
    }

    .batch-upload-section h4 {
      margin-top: 0;
      color: #4d94ff;
    }

    .batch-upload-section input[type="file"] {
      display: none;
    }

    .batch-import-row {
      margin-top: 15px;
      padding-top: 15px;
      border-top: 1px dashed #3a3f55;
    }

    .error-message {
      color: #e74c3c;
      font-size: 12px;
      margin-top: 4px;
    }

    /* 模态框样式 */
    .modal {
      position: fixed;
      z-index: 3000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.7);
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .modal-content {
      background-color: #1f2335;
      border-radius: 8px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
    }
    
    .modal-header {
      padding: 15px;
      border-bottom: 1px solid #323649;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .modal-header h3 {
      margin: 0;
      color: #fff;
    }
    
    .modal-close {
      color: #aaa;
      font-size: 28px;
      font-weight: bold;
      cursor: pointer;
    }
    
    .modal-close:hover {
      color: #fff;
    }
    
    .modal-body {
      padding: 15px;
    }
    
    .full-width-table {
      width: 100%;
      border-collapse: collapse;
    }
    
    .full-width-table th, .full-width-table td {
      padding: 10px;
      text-align: left;
      border-bottom: 1px solid #323649;
    }
    
    .full-width-table th {
      background-color: #323649;
      color: #fff;
    }
    
    .full-width-table tr:hover {
      background-color: #2a2f42;
    }

    /* 滚动条样式优化 */
    ::-webkit-scrollbar {
      width: 10px;
    }

    ::-webkit-scrollbar-track {
      background: #1e2130;
    }

    ::-webkit-scrollbar-thumb {
      background-color: #3a3f55;
      border-radius: 10px;
    }

    ::-webkit-scrollbar-thumb:hover {
      background-color: #4a4f65;
    }
  </style>
</head>

<body>
  <div id="app">
    <div id="sidebar">
      <!-- 主选项卡 -->
      <div class="main-tabs">
        <div class="main-tab" :class="{active: activeMainTab === 'layout'}" @click="activeMainTab = 'layout'">布局管理</div>
        <div class="main-tab" :class="{active: activeMainTab === 'device'}" @click="activeMainTab = 'device'">设备管理</div>
        <div class="main-tab" :class="{active: activeMainTab === 'cabling'}" @click="activeMainTab = 'cabling'">走线管理
        </div>
        <div class="main-tab" :class="{active: activeMainTab === 'report'}" @click="activeMainTab = 'report'">统计报告</div>
      </div>

      <!-- 布局管理选项卡 -->
      <div class="tab-content" :class="{active: activeMainTab === 'layout'}">
        <!-- 机柜管理部分 -->
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('cabinets')">
            机柜管理 <span class="count-badge">{{cabinets.length}}</span>
            <span class="sidebar-arrow" :class="{active: activeSection.cabinets}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.cabinets}">
            <h4>机柜列表</h4>
            <div class="scroll-list" v-if="cabinets.length > 0">
              <div v-for="cab in cabinets" :key="cab.id" class="list-item">
                <span v-if="editingCabinetId !== cab.id">{{cab.name}} ({{cab.u}}U)</span>
                <input v-else v-model="editingCabinetName" @keyup.enter="saveCabinetName(cab.id)"
                  @blur="saveCabinetName(cab.id)" ref="cabinetNameInput" style="width: 100px; margin-right: 5px;" />
                <div>
                  <span class="edit-btn" @click="startEditingCabinetName(cab.id)">重命名</span>
                  <span class="delete-btn" @click="deleteCabinet(cab.id)">删除</span>
                </div>
              </div>
            </div>
            <div v-else>暂无机柜，请先添加机柜</div>

            <h4>列管理</h4>
            <div class="input-row">
              <label>X坐标</label>
              <input type="number" v-model.number="columnPositionX" step="600" />
            </div>
            <div class="input-row">
              <label>Z坐标</label>
              <input type="number" v-model.number="columnPositionZ" step="1000" />
            </div>
            <div class="input-row">
              <label>命名前缀</label>
              <input v-model="columnNaming" placeholder="例如：A-" />
            </div>
            <div class="input-row">
              <label>机柜数量</label>
              <input type="number" v-model.number="columnCabinetCount" min="1" max="20" />
            </div>
            <div class="input-row">
              <label>排列方向</label>
              <select v-model="columnOrientation">
                <option value="horizontal">水平排列 (X轴)</option>
                <option value="vertical">垂直排列 (Z轴)</option>
              </select>
            </div>
            <div class="sub-section">
              <div class="section-header">机柜规格</div>
              <div class="input-row">
                <label>U位数</label>
                <input type="number" v-model.number="columnU" min="1" max="60" />
              </div>
              <div class="input-row">
                <label>宽度</label>
                <input type="number" v-model.number="columnWidth" min="300" max="1200" step="50" />
                <span class="unit">mm</span>
              </div>
              <div class="input-row">
                <label>深度</label>
                <input type="number" v-model.number="columnDepth" min="600" max="1500" step="50" />
                <span class="unit">mm</span>
              </div>
            </div>
            <button class="control-btn" @click="addColumnAtPosition">添加机柜</button>
          </div>
        </div>

        <!-- 走线点系统部分 -->
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('waypoints')">
            走线点系统 <span class="count-badge">{{waypoints.length}}</span>
            <span class="sidebar-arrow" :class="{active: activeSection.waypoints}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.waypoints}">
            <div class="tabs">
              <div class="tab" :class="{active: waypointMode === 'add'}" @click="waypointMode = 'add'">添加走线点</div>
              <div class="tab" :class="{active: waypointMode === 'connect'}" @click="waypointMode = 'connect'">连接走线点
              </div>
            </div>

            <div v-if="waypointMode === 'add'">
              <div class="input-row">
                <label>名称</label>
                <input v-model="waypointName" placeholder="走线点名称" />
              </div>
              <div class="input-row">
                <label>X坐标</label>
                <input type="number" v-model.number="waypointX" step="100" /> <span class="unit">mm</span>
              </div>
              <div class="input-row">
                <label>Y坐标</label>
                <input type="number" v-model.number="waypointY" step="100" /> <span class="unit">mm</span>
              </div>
              <div class="input-row">
                <label>Z坐标</label>
                <input type="number" v-model.number="waypointZ" step="100" /> <span class="unit">mm</span>
              </div>
              <button class="control-btn" @click="addWaypoint">添加走线点</button>
            </div>

            <div v-if="waypointMode === 'connect'">
              <div class="input-row">
                <label>起点</label>
                <select v-model="waypointFrom">
                  <option v-for="wp in waypoints" :value="wp.id">{{wp.name}} ({{wp.x}},{{wp.y}},{{wp.z}})</option>
                </select>
              </div>
              <div class="input-row">
                <label>终点</label>
                <select v-model="waypointTo">
                  <option v-for="wp in waypoints" :value="wp.id">{{wp.name}} ({{wp.x}},{{wp.y}},{{wp.z}})</option>
                </select>
              </div>
              <div class="input-row">
                <label>走线架宽</label>
                <input type="number" v-model.number="cablewayWidth" step="10" min="100" max="500" /> <span
                  class="unit">mm</span>
              </div>
              <button class="control-btn" @click="connectSelectedWaypoints">连接选中走线点</button>
            </div>

            <h4>走线点列表</h4>
            <div class="scroll-list" v-if="waypoints.length > 0">
              <div v-for="wp in waypoints" :key="wp.id" class="list-item">
                <span>{{wp.name}} ({{wp.x}},{{wp.y}},{{wp.z}})</span>
                <span class="delete-btn" @click="deleteWaypoint(wp.id)">删除</span>
              </div>
            </div>
            <div v-else>暂无走线点，请先添加走线点</div>

            <h4>走线架列表</h4>
            <div class="scroll-list" v-if="cableways.length > 0">
              <div v-for="cw in cableways" :key="cw.id" class="list-item">
                <span>
                  {{cw.name}} ({{getCablewayTypeLabel(cw.type)}}) {{cw.width}}mm
                  <small>{{cw.startPoint.x}},{{cw.startPoint.z}}→{{cw.endPoint.x}},{{cw.endPoint.z}}</small>
                </span>
                <span class="delete-btn" @click="deleteCableway(cw.id)">删除</span>
              </div>
            </div>
            <div v-else>暂无走线架，请通过走线点连接创建走线架</div>
          </div>
        </div>

        <!-- 导入导出部分 -->
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('importExport')">
            导入导出
            <span class="sidebar-arrow" :class="{active: activeSection.importExport}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.importExport}">
            <button class="control-btn" @click="exportData">导出布局数据</button>
            <button class="control-btn" @click="importData">导入布局数据</button>
            <a id="download-link" style="display:none"></a>
            <input type="file" id="import-file" style="display:none" accept=".json" @change="handleFileUpload" />
          </div>
        </div>
      </div>

      <!-- 设备管理选项卡 -->
      <div class="tab-content" :class="{active: activeMainTab === 'device'}">
        <!-- 设备管理部分 -->
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('devices')">
            设备管理 <span class="count-badge">{{devices.length}}</span>
            <span class="sidebar-arrow" :class="{active: activeSection.devices}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.devices}">
            <h4>设备管理</h4>
            <div class="info-message">
              请先点击机柜，然后在右侧属性面板中添加设备
            </div>
            
            <!-- 添加批量导入功能 -->
            <div class="batch-upload-section">
              <h4>批量导入设备</h4>
              <div class="info-message">
                通过CSV文件批量导入设备。CSV格式需要以下列：rack_name,device_name,upos,uheight,nports
              </div>
              <input type="file" id="csv-upload" accept=".csv" @change="handleDeviceCSVUpload" />
              <button class="control-btn" @click="triggerCSVFileUpload">选择CSV文件</button>
            </div>

            <h4>设备列表</h4>
            <div class="scroll-list" v-if="devices.length > 0">
              <div v-for="dev in devices" :key="dev.id" class="list-item">
                <span>{{dev.name}} ({{getCabinetName(dev.cabinetId)}} U{{dev.u}}, {{dev.height}}U高)</span>
                <div>
                  <span class="edit-btn" @click="editDevice(dev.id)">编辑</span>
                  <span class="delete-btn" @click="deleteDevice(dev.id)">删除</span>
                </div>
              </div>
            </div>
            <div class="edit-form" v-if="editingDevice">
              <div class="edit-form-title">编辑设备 {{editingDevice.name}}</div>
              <div class="input-row">
                <label>名称</label>
                <input v-model="editingDevice.name" />
              </div>
              <div class="input-row">
                <label>U位</label>
                <input type="number" v-model.number="editingDevice.u" min="1"
                  :max="getCabinetU(editingDevice.cabinetId)" />
              </div>
              <div class="input-row">
                <label>高度(U)</label>
                <input type="number" v-model.number="editingDevice.height" min="1" max="20" />
              </div>
              <div class="input-row">
                <label>端口数</label>
                <input type="number" v-model.number="editingDevicePorts" min="1" max="48" />
              </div>
              <button class="control-btn" @click="saveDeviceEdit">保存</button>
              <button class="control-btn" style="background-color: #999;" @click="cancelDeviceEdit">取消</button>
            </div>
            <div v-else>暂无设备，请先添加设备</div>
          </div>
        </div>
      </div>

      <!-- 走线管理选项卡 -->
      <div class="tab-content" :class="{active: activeMainTab === 'cabling'}">
        <!-- 端口连线部分 -->
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('cabling')">
            端口连线 <span class="count-badge">{{cables.length}}</span>
            <span class="sidebar-arrow" :class="{active: activeSection.cabling}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.cabling}">
            <div class="input-row">
              <label>起点端口</label>
              <select v-model="cableFrom">
                <option v-for="port in allPorts" :value="port.id">{{port.label}}</option>
              </select>
            </div>
            <div class="input-row">
              <label>终点端口</label>
              <select v-model="cableTo">
                <option v-for="port in allPorts" :value="port.id">{{port.label}}</option>
              </select>
            </div>
            <button class="control-btn" @click="addCable">连线</button>

            <h4>线缆列表</h4>
            <div class="scroll-list" v-if="cables.length > 0">
              <div v-for="cable in cables" :key="cable.id" class="list-item">
                <span>
                  {{getPortLabel(cable.from)}} → {{getPortLabel(cable.to)}}
                  ({{cable.length.toFixed(2)}})
                </span>
                <span class="delete-btn" @click="deleteCable(cable.id)">删除</span>
              </div>
            </div>
            <div v-else>暂无线缆，请先添加线缆</div>

            <!-- 添加批量导入线缆功能 -->
            <div class="batch-upload-section">
              <h4>批量导入线缆</h4>
              <div class="info-message">
                通过CSV文件批量导入线缆。CSV格式需要以下列：adev,aport,zdev,zport
              </div>
              <input type="file" id="cable-csv-upload" accept=".csv" @change="handleCableCSVUpload" style="display:none;" />
              <button class="control-btn" @click="triggerCableCSVFileUpload">选择CSV文件</button>
            </div>

            <div v-if="selectedCable" class="cable-info">
              <b>线缆信息</b><br>
              长度: {{selectedCable.length.toFixed(2)}}<br>
              路径: <span v-for="(p,i) in selectedCable.path" :key="i">
                ({{p.x}},{{p.y}},{{p.z}})
                <span v-if="i!==selectedCable.path.length-1">→</span>
              </span>
            </div>
          </div>
        </div>
      </div>

      <!-- 统计报告选项卡 -->
      <div class="tab-content" :class="{active: activeMainTab === 'report'}">
        <div class="sidebar-section">
          <div class="sidebar-header" @click="toggleSection('report')">
            统计报告
            <span class="sidebar-arrow" :class="{active: activeSection.report}">▶</span>
          </div>
          <div class="sidebar-content" :class="{active: activeSection.report}">
                          <button class="control-btn" @click="showCableListModal = true">查看线缆列表</button>

            <h4>冗余设置</h4>
            <div>
              <div class="input-group">
                <label>冗余百分比</label>
                <div class="input-with-unit">
                  <input type="number" v-model="redundancyPercentage" min="0" max="100" step="1" />
                  <span class="input-unit">%</span>
                </div>
              </div>
              <div class="input-group">
                <label>固定冗余</label>
                <div class="input-with-unit">
                  <input type="number" v-model="fixedRedundancy" min="0" max="10" step="0.1" />
                  <span class="input-unit">m</span>
                </div>
              </div>
              <div class="input-group">
                <label>长度间隔</label>
                <div class="input-with-unit">
                  <input type="number" v-model="lengthInterval" min="1" max="50" step="1" />
                  <span class="input-unit">m</span>
                </div>
              </div>
            </div>

                          <h4>线缆长度统计 ({{ lengthInterval }}m间隔，含{{ fixedRedundancy }}m固定冗余 + {{ redundancyPercentage }}%比例冗余)</h4>
            <div class="scroll-list">
              <table>
                <thead>
                  <tr>
                    <th>长度范围 (m)</th>
                    <th>数量</th>
                  </tr>
                </thead>
                <tbody>
                  <tr v-for="stat in Object.entries(cableStatistics)" :key="stat[0]">
                    <td>{{ stat[0] }}</td>
                    <td>{{ stat[1] }}</td>
                  </tr>
                </tbody>
              </table>
              <div v-if="Object.keys(cableStatistics).length === 0">暂无线缆统计数据</div>
            </div>

            <button class="control-btn" @click="exportReportCSV">导出CSV报告</button>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 线缆列表模态框 -->
    <div class="modal" v-if="showCableListModal">
      <div class="modal-content" style="width: 80%; max-width: 1200px; max-height: 80vh; overflow: auto;">
        <div class="modal-header">
          <h3>线缆列表</h3>
          <span class="modal-close" @click="showCableListModal = false">&times;</span>
        </div>
        <div class="modal-body">
          <table class="full-width-table">
            <thead>
              <tr>
                <th>源设备</th>
                <th>源端口</th>
                <th>目的设备</th>
                <th>目的端口</th>
                <th>长度(mm)</th>
                <th>路径</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="cable in cableTableData" :key="cable.id">
                <td>{{ cable.fromDevice }}</td>
                <td>{{ cable.fromPort }}</td>
                <td>{{ cable.toDevice }}</td>
                <td>{{ cable.toPort }}</td>
                <td>{{ cable.length.toFixed(2) }}</td>
                <td>{{ cable.waypointPath }}</td>
              </tr>
            </tbody>
          </table>
          <div v-if="cableTableData.length === 0" style="padding: 20px; text-align: center;">暂无线缆数据</div>
        </div>
      </div>
    </div>
    
    <div id="three-canvas">
      <!-- 操作通知 -->
      <div id="action-notification" class="action-notification">已撤销操作</div>

      <!-- 机柜属性面板 -->
      <div class="cabinet-properties" v-if="selectedCabinet">
        <div class="properties-header">
          <h3>机柜属性</h3>
          <span class="close-btn" @click="selectedCabinet = null">&times;</span>
        </div>
        <div class="properties-content">
          <div class="prop-row">
            <label>名称:</label>
            <input v-model="selectedCabinet.name" @change="updateSelectedCabinet" />
          </div>
          <div class="prop-row">
            <label>U位数:</label>
            <input type="number" v-model.number="selectedCabinet.u" @change="updateSelectedCabinet" />
          </div>
          <div class="prop-row">
            <label>宽度:</label>
            <input type="number" v-model.number="selectedCabinet.width" @change="updateSelectedCabinet" />
            <span class="unit">mm</span>
          </div>
          <div class="prop-row">
            <label>深度:</label>
            <input type="number" v-model.number="selectedCabinet.depth" @change="updateSelectedCabinet" />
            <span class="unit">mm</span>
          </div>
          <div class="prop-row">
            <label>X坐标:</label>
            <input type="number" v-model.number="selectedCabinet.x" @change="updateSelectedCabinet" />
          </div>
          <div class="prop-row">
            <label>Z坐标:</label>
            <input type="number" v-model.number="selectedCabinet.z" @change="updateSelectedCabinet" />
          </div>
          <div class="prop-row">
            <button class="control-btn" @click="selectedCabinet = null">关闭</button>
            <button class="control-btn danger-btn" @click="deleteCabinetAndClose(selectedCabinet.id)">删除机柜</button>
          </div>
        </div>

        <!-- 添加设备到此机柜 -->
        <div class="properties-content">
          <h4>添加设备到此机柜</h4>
          <div class="prop-row">
            <label>设备名称:</label>
            <input type="text" v-model="selectedCabinetDeviceName" placeholder="输入唯一设备名称" />
          </div>
          <div class="prop-row">
            <label>U位:</label>
            <input type="number" v-model.number="selectedCabinetDeviceU" min="1" :max="selectedCabinet.u" />
          </div>
          <div class="prop-row">
            <label>高度(U):</label>
            <input type="number" v-model.number="selectedCabinetDeviceHeight" min="1" max="20" />
          </div>
          <div class="prop-row">
            <label>端口数:</label>
            <input type="number" v-model.number="selectedCabinetDevicePorts" min="1" max="48" />
          </div>
          <div class="prop-row">
            <button class="control-btn" @click="addDeviceToSelectedCabinet">添加设备</button>
          </div>
          
          <!-- 添加批量导入功能到机柜属性面板 -->
          <div class="prop-row batch-import-row">
            <label>批量导入:</label>
            <button class="control-btn" @click="triggerCSVFileUploadForCabinet">从CSV导入</button>
            <input type="file" id="csv-upload-cabinet" accept=".csv" @change="handleDeviceCSVUploadForCabinet" style="display:none;" />
          </div>
        </div>
      </div>

      <div id="axis-info">
        <div><span class="axis-color x-color"></span>X轴 - 右方向 (红色)</div>
        <div><span class="axis-color y-color"></span>Y轴 - 上方向 (绿色)</div>
        <div><span class="axis-color z-color"></span>Z轴 - 前方向 (蓝色)</div>
      </div>
      <div id="keyboard-controls">
        键盘控制: <br>
        ←→↑↓ 平移视图<br>
        1-正视图 2-侧视图 3-顶视图 4-等轴测图<br>
        Ctrl+Z 撤销操作 Ctrl+Y 恢复操作
      </div>
      <div class="view-options">
        <div>视图选项：</div>
        <div>
          <span>显示标签</span>
          <div class="toggle-switch">
            <input type="checkbox" id="show-labels" v-model="showLabels" />
            <label for="show-labels"></label>
          </div>
        </div>
        <div>
          <span>显示连接点</span>
          <div class="toggle-switch">
            <input type="checkbox" id="show-connectors" v-model="showConnectors" />
            <label for="show-connectors"></label>
          </div>
        </div>
        <div>
          <span>显示走线架</span>
          <div class="toggle-switch">
            <input type="checkbox" id="show-cableways" v-model="showCableways" />
            <label for="show-cableways"></label>
          </div>
        </div>
      </div>

      

      <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
      <script src="https://unpkg.com/three@0.153.0/build/three.min.js"></script>
      <script>
        const { createApp, ref, computed } = Vue;

        createApp({
          setup() {
            // 数据
            const cabinets = ref([]);
            const devices = ref([]);
            const patchPanels = ref([]);
            const cableways = ref([]);
            const cables = ref([]);
                          const selectedCable = ref(null);

            // 设备编辑相关
            const editingDevice = ref(null);
            const editingDevicePorts = ref(0);

            // 机柜重命名相关
            const editingCabinetId = ref(null);
            const editingCabinetName = ref('');

            // 3D视图中选中的机柜
            const selectedCabinet = ref(null);

            // 添加到选中机柜的设备表单
            const selectedCabinetDeviceU = ref(1);
            const selectedCabinetDeviceHeight = ref(1);
            const selectedCabinetDevicePorts = ref(4);
            const selectedCabinetDeviceName = ref('');

            // 撤销和恢复功能相关变量
            const historyStates = ref([]); // 历史状态
            const currentHistoryIndex = ref(-1); // 当前历史索引
            const isUndoRedoOperation = ref(false); // 是否正在执行撤销/恢复操作

            // 侧边栏UI控制
            const activeMainTab = ref('layout');
            const activeSection = ref({
              cabinets: true,
              waypoints: false,
              importExport: true,
              devices: true,
              cabling: true,
              report: true
            });

            function toggleSection(section) {
              activeSection.value[section] = !activeSection.value[section];
            }

            // 全局配置参数
            const config = {
              cabinet: {
                defaultWidth: 600,    // 机柜默认宽度，单位mm
                defaultDepth: 1400,   // 机柜默认深度，单位mm
                defaultU: 52,         // 机柜默认U位数
                uHeight: 44.45,       // 每U高度，单位mm (标准1U = 44.45mm)
                holeSize: 150         // 顶部开孔尺寸，单位mm
              },
              device: {
                defaultWidth: 500,    // 设备默认宽度
                defaultDepth: 800,    // 设备默认深度
                defaultHeight: 1,     // 设备默认高度（U）
                portOffset: 200       // 端口在机柜前方的偏移
              },
              cableway: {
                defaultWidth: 240,    // 走线架默认宽度
                defaultHeight: 50,    // 走线架默认高度
                connectionRadius: 10, // 连接点半径
                holeSize: 120         // 走线架开孔尺寸，单位mm
              }
            };

            // 相机控制变量
            const panX = ref(0);
            const panY = ref(0);
            const panZ = ref(0);
            let theta = Math.PI / 4; // 初始角度 
            let phi = Math.PI / 4;   // 调整初始仰角，稍微俯视一点
            let cameraDistance = 30000;
            let cameraTarget = new THREE.Vector3(0, 0, 0); // 相机目标点

            // 相机和场景变量
            let scene = null;
            let camera = null;
            let renderer = null;
            let raycaster = null;
            let mouse = null;

            // 相机更新函数 - 将在初始化后重新赋值
            let updateCamera = () => { };

            // 平移相机函数
            function panCamera(dx, dy, dz) {
              // 创建移动向量
              const moveVector = new THREE.Vector3(dx, dy, dz);

              // 计算相机的局部坐标系方向
              const cameraForward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
              const cameraRight = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
              const cameraUp = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);

              // 计算最终移动向量
              const movement = new THREE.Vector3()
                .addScaledVector(cameraRight, moveVector.x)
                .addScaledVector(cameraUp, moveVector.y)
                .addScaledVector(cameraForward, -moveVector.z); // 负号是因为forward是朝z轴负方向

              // 同时移动相机和目标点，保持视角不变
              camera.position.add(movement);
              cameraTarget.add(movement);
              camera.lookAt(cameraTarget);

              // 渲染更新
              if (renderer) {
                renderer.render(scene, camera);
              }
            }

            // 表单
            const cabinetName = ref('');
            const cabinetU = ref(config.cabinet.defaultU);
            const cabinetX = ref(0);
            const cabinetY = ref(0);
            const cabinetZ = ref(0);
            const cabinetWidth = ref(config.cabinet.defaultWidth);
            const cabinetDepth = ref(config.cabinet.defaultDepth);
            const deviceCabinet = ref('');
            const deviceU = ref(1);
            const deviceHeight = ref(config.device.defaultHeight);
            const devicePorts = ref(4);
            const patchPanelHeight = ref(200);
            const patchPanelWidth = ref(800);
            const patchPanelDepth = ref(300);
            const patchPanelType = ref('global');
            const patchPanelCabinet = ref('');
            const cableFrom = ref('');
            const cableTo = ref('');


            // 新增表单字段 - 走线架系统
            const cablewayMode = ref('simple');
            const cablewayType = ref('linear');
            const cablewayName = ref('');
            const cablewayStartX = ref(0);
            const cablewayStartY = ref(1000);
            const cablewayStartZ = ref(0);
            const cablewayEndX = ref(800);
            const cablewayEndY = ref(1000);
            const cablewayEndZ = ref(0);
            const cablewayWidth = ref(config.cableway.defaultWidth);
            const cablewayHeight = ref(config.cableway.defaultHeight);

            // 视图控制选项
            const showLabels = ref(false);      // 是否显示标签
            const showConnectors = ref(false);  // 是否显示连接点
            const showCableways = ref(true);  // 是否显示走线架

            // 端口列表
            const allPorts = computed(() => {
              return devices.value.flatMap(dev => {
                return dev.ports.map((p, idx) => ({
                  id: dev.id + '-' + idx,
                  label: dev.name + ' ' + (idx + 1) + ' (' + getCabinetName(dev.cabinetId) + ' U' + dev.u + ')',
                  devId: dev.id,
                  portIdx: idx,
                  pos: getPortPos(dev, idx)
                }));
              });
            });

            // 获取端口标签
            function getPortLabel(portId) {
              const port = allPorts.value.find(p => p.id === portId);
              return port ? port.label : '未知端口';
            }

            // 工具函数
            function getCabinetName(id) {
              const cab = cabinets.value.find(c => c.id === id);
              return cab ? cab.name : '';
            }

            // 新增函数：根据端口ID获取端口详细信息
            function getPortDetails(portId) {
              const port = allPorts.value.find(p => p.id === portId);
              if (!port) return { deviceName: '未知设备', portNumber: '未知端口', cabinetName: '未知机柜' };

              const device = devices.value.find(d => d.id === port.devId);
              const cabinet = device ? cabinets.value.find(c => c.id === device.cabinetId) : null;

              return {
                deviceName: device ? device.name : '未知设备',
                // 端口索引从0开始，但我们显示端口号从1开始
                portNumber: port.portIdx + 1,
                cabinetName: cabinet ? cabinet.name : '未知机柜'
              };
            }

            function getCabinetU(id) {
              const cab = cabinets.value.find(c => c.id === id);
              return cab ? cab.u : 52;
            }

            function getPortPos(dev, idx) {
              // 使用机柜的实际位置
              const cab = cabinets.value.find(c => c.id === dev.cabinetId);
              if (!cab) return { x: 0, y: 0, z: 0 };

              // 检查机柜是否旋转
              const cabinetRotation = cab.rotation || 0;
              const isRotated = cabinetRotation === 90;

              // 计算设备中间高度位置
              const deviceHeight = dev.height || 1; // 默认1U高
              const deviceMiddleU = dev.u + (deviceHeight - 1) / 2;
              const y = cab.y + deviceMiddleU * config.cabinet.uHeight + 22;

              // 计算端口的分布
              const deviceWidth = config.device.defaultWidth;
              const deviceDepth = config.device.defaultDepth;
              const totalPorts = dev.ports.length;

              if (isRotated) {
                // 当机柜旋转90度时，端口应该面向X轴正方向
                // 端口沿Z轴排列
                const portSpacing = deviceDepth * 0.8 / Math.max(totalPorts, 1); // 留出10%边距
                const startZ = cab.z - (deviceDepth * 0.4) + (portSpacing / 2);
                const z = startZ + idx * portSpacing;

                // X坐标是固定的，位于机柜正面
                const x = cab.x + config.device.portOffset;

                return { x, y, z };
              } else {
                // 正常情况，端口面向Z轴正方向
                // 端口沿X轴排列
                const portSpacing = deviceWidth * 0.8 / Math.max(totalPorts, 1); // 留出10%边距
                const startX = cab.x - (deviceWidth * 0.4) + (portSpacing / 2);
                const x = startX + idx * portSpacing;

                // Z坐标是固定的，位于机柜正面
                const z = cab.z + config.device.portOffset;

                return { x, y, z };
              }
            }
            function getPatchPanelPos(pp) {
              if (pp.type === 'cabinet') {
                // 机柜顶部走线架
                const cab = cabinets.value.find(c => c.id === pp.cabinetId);
                if (!cab) return { x: 0, y: 0, z: 0 };

                return {
                  x: cab.x,
                  y: cab.y + cab.u * config.cabinet.uHeight + pp.height, // Y轴为高度
                  z: cab.z
                };
              } else {
                // 全局走线架 - 位于所有机柜上方
                if (cabinets.value.length === 0) return { x: 0, y: 0, z: 0 };

                // 计算所有机柜的中心位置
                const avgX = cabinets.value.reduce((sum, cab) => sum + cab.x, 0) / cabinets.value.length;
                const avgZ = cabinets.value.reduce((sum, cab) => sum + cab.z, 0) / cabinets.value.length;
                const maxU = Math.max(...cabinets.value.map(c => c.u), 0);
                const maxHeight = Math.max(...cabinets.value.map(c => c.y + c.u * config.cabinet.uHeight), 0);

                return {
                  x: avgX,
                  y: maxHeight + pp.height, // 最高机柜顶部加上高度偏移
                  z: avgZ
                };
              }
            }

            // 添加机柜
            function addCabinet() {
              if (!cabinetName.value) return;

              // 检查是否存在同名机柜
              const existingCabinet = cabinets.value.find(cab => cab.name === cabinetName.value);
              if (existingCabinet) {
                alert('已存在同名机柜，请使用其他名称');
                return;
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              cabinets.value.push({
                id: 'cab-' + Date.now() + Math.random(),
                name: cabinetName.value,
                u: cabinetU.value,
                x: cabinetX.value,
                y: cabinetY.value,
                z: cabinetZ.value,
                width: cabinetWidth.value,
                depth: cabinetDepth.value
              });
              cabinetName.value = '';
            }



            // 删除机柜
            function deleteCabinet(id) {
              // 保存当前状态以便撤销
              saveStateToHistory();

              // 删除机柜内的设备
              const devicesToDelete = devices.value.filter(dev => dev.cabinetId === id);
              devicesToDelete.forEach(dev => {
                // 注意：这里不再调用deleteDevice，因为它会重复保存状态
                // 直接删除设备相关的线缆和设备
                const portIds = allPorts.value
                  .filter(p => p.devId === dev.id)
                  .map(p => p.id);

                cables.value = cables.value.filter(cable =>
                  !portIds.includes(cable.from) && !portIds.includes(cable.to)
                );
              });

              // 从设备数组中删除所有与此机柜相关的设备
              devices.value = devices.value.filter(dev => dev.cabinetId !== id);

              // 删除机柜
              cabinets.value = cabinets.value.filter(cab => cab.id !== id);
            }

            // 添加设备 - 支持指定高度
            function addDevice() {
              if (!deviceCabinet.value) return;

              // 验证设备高度和U位
              if (!deviceHeight.value || deviceHeight.value < 1) {
                deviceHeight.value = 1; // 默认至少1U高
              }

              const maxU = getCabinetU(deviceCabinet.value);
              if (deviceU.value + deviceHeight.value - 1 > maxU) {
                alert(`设备太高，无法放入该机柜。当前机柜只有${maxU}U，设备需要${deviceHeight.value}U高度。`);
                return;
              }

              // 检查是否与现有设备冲突
              const existingDevices = devices.value.filter(dev => dev.cabinetId === deviceCabinet.value);
              for (const dev of existingDevices) {
                const devStart = dev.u;
                const devEnd = dev.u + (dev.height || 1) - 1;
                const newDevStart = deviceU.value;
                const newDevEnd = deviceU.value + deviceHeight.value - 1;

                // 检查是否有重叠
                if (!(newDevEnd < devStart || newDevStart > devEnd)) {
                  alert(`新设备与现有设备 ${dev.name} (U${dev.u}~U${devEnd}) 位置冲突`);
                  return;
                }
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              const devId = 'dev-' + Date.now() + Math.random();
              devices.value.push({
                id: devId,
                name: '设备' + (devices.value.length + 1),
                cabinetId: deviceCabinet.value,
                u: deviceU.value,
                height: deviceHeight.value,
                ports: Array(devicePorts.value).fill(0)
              });
            }

            // 删除设备
            function deleteDevice(id) {
              // 保存当前状态以便撤销
              saveStateToHistory();

              // 删除与此设备相关的所有线缆
              const portIds = allPorts.value
                .filter(p => p.devId === id)
                .map(p => p.id);

              cables.value = cables.value.filter(cable =>
                !portIds.includes(cable.from) && !portIds.includes(cable.to)
              );

              // 删除设备
              devices.value = devices.value.filter(dev => dev.id !== id);
            }

            // 添加走线架
            function addPatchPanel() {
              const newPatchPanel = {
                id: 'pp-' + Date.now(),
                type: patchPanelType.value,
                height: patchPanelHeight.value,
                width: patchPanelWidth.value,
                depth: patchPanelDepth.value
              };

              if (patchPanelType.value === 'cabinet') {
                if (!patchPanelCabinet.value) return;
                newPatchPanel.cabinetId = patchPanelCabinet.value;
              }

              newPatchPanel.pos = getPatchPanelPos(newPatchPanel);
              patchPanels.value.push(newPatchPanel);
            }

            // 删除走线架
            function deletePatchPanel(id) {
              patchPanels.value = patchPanels.value.filter(pp => pp.id !== id);
            }

            // 获取走线架类型标签
            function getCablewayTypeLabel(type) {
              const types = {
                'linear': '直线走线架',
                'cross': '十字走线架',
                'tshape': 'T型走线架',
                'corner': '弯角走线架'
              };
              return types[type] || '未知类型';
            }

            // 添加高级走线架
            function addCableway() {
              if (!cablewayName.value) {
                alert('请输入走线架名称');
                return;
              }

              // 创建起点和终点
              const startPoint = {
                x: cablewayStartX.value,
                y: cablewayStartY.value,
                z: cablewayStartZ.value
              };

              const endPoint = {
                x: cablewayEndX.value,
                y: cablewayEndY.value,
                z: cablewayEndZ.value
              };

              // 计算连接点 - 根据走线架类型有不同的连接点布局
              let connectionPoints = [];

              if (cablewayType.value === 'linear') {
                // 直线走线架有两个连接点，起点和终点
                connectionPoints = [startPoint, endPoint];
              } else if (cablewayType.value === 'cross') {
                // 十字走线架有四个连接点
                const centerX = (startPoint.x + endPoint.x) / 2;
                const centerY = (startPoint.y + endPoint.y) / 2;
                const centerZ = (startPoint.z + endPoint.z) / 2;

                connectionPoints = [
                  startPoint,
                  endPoint,
                  { x: centerX, y: centerY, z: startPoint.z - (endPoint.z - startPoint.z) }, // 垂直方向点1
                  { x: centerX, y: centerY, z: endPoint.z + (endPoint.z - startPoint.z) }    // 垂直方向点2
                ];
              } else if (cablewayType.value === 'tshape') {
                // T型走线架有三个连接点
                const centerX = (startPoint.x + endPoint.x) / 2;
                const centerY = (startPoint.y + endPoint.y) / 2;
                const centerZ = (startPoint.z + endPoint.z) / 2;

                connectionPoints = [
                  startPoint,
                  endPoint,
                  { x: centerX, y: centerY, z: startPoint.z - (endPoint.z - startPoint.z) } // 垂直方向点
                ];
              } else if (cablewayType.value === 'corner') {
                // 弯角走线架有两个连接点，一个水平，一个垂直
                connectionPoints = [
                  startPoint,
                  { x: startPoint.x, y: startPoint.y, z: endPoint.z } // 弯角点
                ];
              }

              const newCableway = {
                id: 'cw-' + Date.now() + Math.random(),
                name: cablewayName.value,
                type: cablewayType.value,
                startPoint: startPoint,
                endPoint: endPoint,
                width: cablewayWidth.value,
                height: cablewayHeight.value,
                connectionPoints: connectionPoints
              };

              cableways.value.push(newCableway);
              cablewayName.value = '';
            }

            // 删除走线架
            function deleteCableway(id) {
              // 保存当前状态以便撤销
              saveStateToHistory();

              cableways.value = cableways.value.filter(cw => cw.id !== id);
            }

            // 计算线缆路径
            function calculateCablePath(fromPort, toPort) {
              const from = allPorts.value.find(p => p.id === fromPort);
              const to = allPorts.value.find(p => p.id === toPort);
              if (!from || !to) return { path: [], length: 0 };

              // 获取设备所在机柜
              const fromDevice = devices.value.find(d => d.id === from.devId);
              const toDevice = devices.value.find(d => d.id === to.devId);
              if (!fromDevice || !toDevice) return { path: [], length: 0 };

              const fromCabinetId = fromDevice.cabinetId;
              const toCabinetId = toDevice.cabinetId;

              // 计算路径
              let path = [];
              let length = 0;

              // 同设备同端口，或者同设备不同端口时，直连
              if (from.devId === to.devId) {
                path = [from.pos, to.pos];
                length = dist(from.pos, to.pos);
              } 
              // 同一机柜内的设备直连，不通过走线架
              else if (fromCabinetId === toCabinetId) {
                path = [from.pos, to.pos];
                length = dist(from.pos, to.pos);
              } else {
                // 跨机柜设备，必须经过走线架
                // 查找最佳路径
                const result = findBestPathViaCableways(from.pos, to.pos);

                // 如果找不到有效的走线架路径，不允许连接
                if (!result.path || result.path.length <= 1) {
                  return { path: [], length: 0 };
                }

                path = result.path;
                length = result.length;
              }

              return { path, length };
            }

            // 检查线段是否位于走线架内的辅助函数
            function isSegmentInCableway(segment, cableway) {
              // 计算线段的中点
              const midpoint = {
                x: (segment.start.x + segment.end.x) / 2,
                y: (segment.start.y + segment.end.y) / 2,
                z: (segment.start.z + segment.end.z) / 2
              };

              // 简化检查 - 检查线段两端点是否足够接近走线架连接点
              // 如果线段的任一端点是连接点附近，则认为是合法的
              for (const point of cableway.connectionPoints) {
                // 检查开始点
                const startDistance = Math.sqrt(
                  Math.pow(segment.start.x - point.x, 2) +
                  Math.pow(segment.start.y - point.y, 2) +
                  Math.pow(segment.start.z - point.z, 2)
                );

                // 检查结束点
                const endDistance = Math.sqrt(
                  Math.pow(segment.end.x - point.x, 2) +
                  Math.pow(segment.end.y - point.y, 2) +
                  Math.pow(segment.end.z - point.z, 2)
                );

                // 如果任一端点在连接点附近，认为有效
                if (startDistance < 100 || endDistance < 100) {
                  return true;
                }
              }

              // 更宽松地检查是否在走线架内
              // 计算走线架的边界（考虑更大的容差）
              const tolerance = 50; // 增加容差，单位毫米
              const cwMinX = Math.min(cableway.startPoint.x, cableway.endPoint.x) - cableway.width / 2 - tolerance;
              const cwMaxX = Math.max(cableway.startPoint.x, cableway.endPoint.x) + cableway.width / 2 + tolerance;
              const cwMinY = Math.min(cableway.startPoint.y, cableway.endPoint.y) - cableway.height / 2 - tolerance;
              const cwMaxY = Math.max(cableway.startPoint.y, cableway.endPoint.y) + cableway.height / 2 + tolerance;
              const cwMinZ = Math.min(cableway.startPoint.z, cableway.endPoint.z) - cableway.width / 2 - tolerance;
              const cwMaxZ = Math.max(cableway.startPoint.z, cableway.endPoint.z) + cableway.width / 2 + tolerance;

              // 检查中点是否在走线架的容差边界内
              const isWithinBounds =
                midpoint.x >= cwMinX && midpoint.x <= cwMaxX &&
                midpoint.y >= cwMinY && midpoint.y <= cwMaxY &&
                midpoint.z >= cwMinZ && midpoint.z <= cwMaxZ;

              return isWithinBounds;
            }

            // 查找通过走线架的最佳路径
            function findBestPathViaCableways(startPos, endPos) {
              // 获取起点和终点所在的机柜ID（如果有）
              const startCabinetId = findCabinetIdByPosition(startPos);
              const endCabinetId = findCabinetIdByPosition(endPos);

              // 同一机柜内的设备直接连接，不通过走线架
              if (startCabinetId && endCabinetId && startCabinetId === endCabinetId) {
                console.log("同一机柜内设备直连:", startPos, endPos);
                return {
                  path: [startPos, endPos],
                  length: dist(startPos, endPos)
                };
              }

              // 如果没有走线架系统，无法完成跨机柜连接
              if (cableways.value.length === 0) {
                alert('没有走线架系统，无法完成跨机柜连接，请先添加走线架');
                return { path: [], length: 0 };
              }

              try {
                // 寻找起点和终点正上方最近的走线架点
                const startOverheadResult = findNearestCablewayPointAbove(startPos);
                if (!startOverheadResult) {
                  alert('找不到起点上方的走线架，请确保有走线架覆盖该区域');
                  return { path: [], length: 0 };
                }

                const endOverheadResult = findNearestCablewayPointAbove(endPos);
                if (!endOverheadResult) {
                  alert('找不到终点上方的走线架，请确保有走线架覆盖该区域');
                  return { path: [], length: 0 };
                }

                // 获取走线架实例
                const startCableway = cableways.value.find(c => c.id === startOverheadResult.cablewayId);
                const endCableway = cableways.value.find(c => c.id === endOverheadResult.cablewayId);

                if (!startCableway || !endCableway) {
                  console.error('找不到对应的走线架');
                  return { path: [], length: 0 };
                }

                // 确保连接点在走线架内部
                let startConnectPoint = { ...startOverheadResult.point };
                let endConnectPoint = { ...endOverheadResult.point };

                // 构建路径
                const path = [
                  // 起点
                  startPos,

                  // 垂直上升到走线架高度
                  {
                    x: startPos.x,
                    y: startConnectPoint.y,
                    z: startPos.z
                  },

                  // 走线架入口点
                  startConnectPoint
                ];

                // 如果起点和终点不在同一走线架，则计算走线架间的最短路径
                if (startOverheadResult.cablewayId !== endOverheadResult.cablewayId) {
                  // 查找走线架之间的路径
                  const cablewayPath = findPathBetweenCableways(
                    startOverheadResult.cablewayId,
                    startConnectPoint,
                    endOverheadResult.cablewayId,
                    endConnectPoint
                  );

                  if (!cablewayPath || cablewayPath.length === 0) {
                    alert('走线架之间无法找到连通路径');
                    return { path: [], length: 0 };
                  }

                  // 添加中间走线架路径点 (跳过第一个点，因为已经添加了)
                  path.push(...cablewayPath.slice(1));
                }

                // 如果最后一个点不是终点的走线架连接点，添加它
                const lastPoint = path[path.length - 1];
                if (lastPoint.x !== endConnectPoint.x ||
                  lastPoint.y !== endConnectPoint.y ||
                  lastPoint.z !== endConnectPoint.z) {
                  path.push(endConnectPoint);
                }

                // 从走线架垂直下降到终点位置
                path.push(
                  // 垂直下降到目标位置的上方
                  {
                    x: endPos.x,
                    y: endConnectPoint.y,
                    z: endPos.z
                  },

                  // 终点
                  endPos
                );

                // 计算路径长度
                let length = 0;
                for (let i = 0; i < path.length - 1; i++) {
                  length += dist(path[i], path[i + 1]);
                }

                return { path, length };
              } catch (error) {
                console.error('计算走线路径时出错:', error);
                return { path: [], length: 0 };
              }
            }

            // 寻找点正上方最近的走线架位置
            function findNearestCablewayPointAbove(pos) {
              // 获取所有走线架
              if (cableways.value.length === 0) return null;

              let nearestPoint = null;
              let nearestDistance = Infinity;
              let cablewayId = null;
              let minHorizontalDist = Infinity;
              let isNearestDirectlyAbove = false;

              // 找出所有高于该点的走线架
              for (const cw of cableways.value) {
                // 确保走线架的高度大于点的高度
                if (cw.startPoint.y <= pos.y && cw.endPoint.y <= pos.y) continue;

                // 计算走线架在XZ平面的投影与点的水平距离
                const cwStartX = cw.startPoint.x;
                const cwStartZ = cw.startPoint.z;
                const cwEndX = cw.endPoint.x;
                const cwEndZ = cw.endPoint.z;

                // 计算水平距离 - 点到线段的最短距离
                const horizontalDist = distancePointToLine(pos.x, pos.z, cwStartX, cwStartZ, cwEndX, cwEndZ);

                // 确保点在走线架宽度范围内，加上一点余量
                const directlyAboveThreshold = cw.width * 0.6; // 稍微大于走线架宽度的一半
                const isDirectlyAbove = horizontalDist <= directlyAboveThreshold;

                // 获取走线架的Y坐标 - 使用走线架的最低点
                const cwHeight = Math.min(cw.startPoint.y, cw.endPoint.y);
                const verticalDist = cwHeight - pos.y;

                // 如果在正上方或是最近走线架，且高度为正
                if (verticalDist > 0 &&
                  (isDirectlyAbove || horizontalDist < minHorizontalDist)) {

                  // 判断是否更新最近走线架
                  if ((isDirectlyAbove && verticalDist < nearestDistance) ||
                    (!nearestPoint || !isNearestDirectlyAbove) ||
                    (isDirectlyAbove && isNearestDirectlyAbove && verticalDist < nearestDistance)) {

                    nearestDistance = verticalDist;

                    // 计算在走线架正中间的投影点位置
                    const t = projectPointOnLine(pos.x, pos.z, cwStartX, cwStartZ, cwEndX, cwEndZ);

                    // 确保t在[0,1]范围内，即投影在走线架段上
                    const clampedT = Math.max(0, Math.min(1, t));

                    // 计算确切位置 - 在走线架上的投影点
                    const projectedX = cwStartX + clampedT * (cwEndX - cwStartX);
                    const projectedZ = cwStartZ + clampedT * (cwEndZ - cwStartZ);

                    // 投影Y坐标使用走线架的底部（确保在走线架内部）
                    const projectedY = cw.startPoint.y + clampedT * (cw.endPoint.y - cw.startPoint.y);

                    // 调整Y坐标，确保在走线架内（稍微向上移动）
                    const adjustedY = projectedY - (cw.height * 0.2);

                    nearestPoint = {
                      x: projectedX,
                      y: adjustedY,
                      z: projectedZ
                    };
                    cablewayId = cw.id;
                    isNearestDirectlyAbove = isDirectlyAbove;
                    minHorizontalDist = horizontalDist;
                  }
                }
              }

              // 如果没找到任何走线架，返回null
              if (!nearestPoint) {
                return null;
              }

              return { point: nearestPoint, cablewayId };
            }

            // 计算点在线段上的投影参数t (0-1之间表示在线段上，小于0表示在起点之前，大于1表示在终点之后)
            function projectPointOnLine(px, pz, x1, z1, x2, z2) {
              const dx = x2 - x1;
              const dz = z2 - z1;

              // 如果线段长度为0，返回0
              if (dx === 0 && dz === 0) return 0;

              // 计算投影点的t参数值 
              const t = ((px - x1) * dx + (pz - z1) * dz) / (dx * dx + dz * dz);

              // 限制t在[0,1]范围内，表示投影点在线段上
              return Math.max(0, Math.min(1, t));
            }

            // 直接在走线架中连接起点和终点
            function findDirectPathBetweenCableways(startCablewayId, startPoint, endCablewayId, endPoint) {
              // 如果是同一走线架，直接连接
              if (startCablewayId === endCablewayId) {
                return [startPoint, endPoint];
              }

              // 构建走线架连接图
              const cablewayGraph = buildCablewayGraph();

              // 使用广度优先搜索查找走线架间的最短路径
              const path = findShortestPath(cablewayGraph, startCablewayId, endCablewayId);

              if (!path || path.length === 0) {
                console.error('走线架之间找不到路径:', startCablewayId, endCablewayId);
                return [];
              }

              // 收集路径上的交叉点
              const pathPoints = [];
              pathPoints.push(startPoint);

              // 查找走线架对象
              const allCableways = path.map(id => cableways.value.find(cw => cw.id === id)).filter(Boolean);

              // 添加中间交叉点
              for (let i = 0; i < path.length - 1; i++) {
                const cablewayId1 = path[i];
                const cablewayId2 = path[i + 1];

                const cableway1 = cableways.value.find(cw => cw.id === cablewayId1);
                const cableway2 = cableways.value.find(cw => cw.id === cablewayId2);

                if (cableway1 && cableway2) {
                  // 找到两个走线架的交叉点
                  const intersection = findBestConnectionPoint(cableway1, cableway2);

                  if (intersection) {
                    // 确保交叉点在走线架内部 - 向下适当偏移
                    const adjustedIntersection = {
                      x: intersection.x,
                      y: intersection.y - Math.min(cableway1.height, cableway2.height) * 0.25,
                      z: intersection.z
                    };

                    pathPoints.push(adjustedIntersection);
                  }
                }
              }

              // 添加终点
              pathPoints.push(endPoint);

              // 检查路径是否包含足够的点
              if (pathPoints.length < 2) {
                console.error('生成的路径点不足');
                return [startPoint, endPoint];
              }

              return pathPoints;
            }

            // 查找走线架之间的路径
            function findPathBetweenCableways(startCablewayId, startPoint, endCablewayId, endPoint) {
              // 如果是同一个走线架，只需要沿着走线架走
              if (startCablewayId === endCablewayId) {
                return [startPoint, endPoint];
              }

              // 构建走线架连接图
              const cablewayGraph = buildCablewayGraph();

              // 使用广度优先搜索查找走线架间的最短路径
              const path = findShortestPath(cablewayGraph, startCablewayId, endCablewayId);

              // 如果找不到路径，返回空数组
              if (!path || path.length === 0) {
                console.warn('在走线架网络中找不到从', startCablewayId, '到', endCablewayId, '的路径');
                return [];
              }

              // 将抽象路径转换为具体坐标点
              const pathPoints = [];

              // 添加起点 - 这是走线架上的连接点
              pathPoints.push(startPoint);

              // 处理路径中的每个走线架节点交叉点
              for (let i = 0; i < path.length - 1; i++) {
                const currentCablewayId = path[i];
                const nextCablewayId = path[i + 1];

                // 获取走线架
                const currentCableway = cableways.value.find(cw => cw.id === currentCablewayId);
                const nextCableway = cableways.value.find(cw => cw.id === nextCablewayId);

                if (!currentCableway || !nextCableway) continue;

                // 查找当前走线架与下一个走线架的交叉点
                const intersection = findBestConnectionPoint(currentCableway, nextCableway);

                if (intersection) {
                  // 添加交叉点 - 直接添加，不做额外的对齐
                  pathPoints.push(intersection);
                }
              }

              // 添加终点 - 这是走线架上的连接点
              if (!pointsEqual(pathPoints[pathPoints.length - 1], endPoint)) {
                pathPoints.push(endPoint);
              }

              return pathPoints;
            }

            // 新增函数：将点对齐到走线架轴线上
            function alignPointToCablewayAxis(point, cableway) {
              // 计算走线架的方向向量
              const direction = {
                x: cableway.endPoint.x - cableway.startPoint.x,
                y: cableway.endPoint.y - cableway.startPoint.y,
                z: cableway.endPoint.z - cableway.startPoint.z
              };

              // 计算方向向量的长度
              const dirLength = Math.sqrt(
                direction.x * direction.x +
                direction.y * direction.y +
                direction.z * direction.z
              );

              // 单位化方向向量
              if (dirLength > 0) {
                direction.x /= dirLength;
                direction.y /= dirLength;
                direction.z /= dirLength;
              }

              // 计算点到走线架起点的向量
              const v = {
                x: point.x - cableway.startPoint.x,
                y: point.y - cableway.startPoint.y,
                z: point.z - cableway.startPoint.z
              };

              // 计算向量v在走线架方向上的投影长度
              const projLength = v.x * direction.x + v.y * direction.y + v.z * direction.z;

              // 计算投影点坐标 - 这个点在走线架的中轴线上
              const projPoint = {
                x: cableway.startPoint.x + direction.x * projLength,
                y: cableway.startPoint.y + direction.y * projLength,
                z: cableway.startPoint.z + direction.z * projLength
              };

              return projPoint;
            }

            // 新增函数：查找两个走线架之间的最佳连接点
            function findBestConnectionPoint(cableway1, cableway2) {
              // 首先检查是否有预定义的交叉点（由检测算法创建）
              for (const cp1 of cableway1.connectionPoints) {
                for (const cp2 of cableway2.connectionPoints) {
                  if (pointsEqual(cp1, cp2, 20)) { // 使用更大的容差
                    return cp1; // 返回找到的交叉点
                  }
                }
              }

              // 如果没有预定义的交叉点，尝试计算两个走线架的"最近点"
              // 找到两个走线架中轴线上最接近的两个点

              // 在第一个走线架上取多个点
              const points1 = [];
              const segments = 10;
              for (let i = 0; i <= segments; i++) {
                const t = i / segments;
                points1.push({
                  x: cableway1.startPoint.x + (cableway1.endPoint.x - cableway1.startPoint.x) * t,
                  y: cableway1.startPoint.y + (cableway1.endPoint.y - cableway1.startPoint.y) * t,
                  z: cableway1.startPoint.z + (cableway1.endPoint.z - cableway1.startPoint.z) * t
                });
              }

              // 在第二个走线架上取多个点
              const points2 = [];
              for (let i = 0; i <= segments; i++) {
                const t = i / segments;
                points2.push({
                  x: cableway2.startPoint.x + (cableway2.endPoint.x - cableway2.startPoint.x) * t,
                  y: cableway2.startPoint.y + (cableway2.endPoint.y - cableway2.startPoint.y) * t,
                  z: cableway2.startPoint.z + (cableway2.endPoint.z - cableway2.startPoint.z) * t
                });
              }

              // 找到两组点之间距离最小的一对
              let minDist = Infinity;
              let bestPoint1 = null;
              let bestPoint2 = null;

              for (const p1 of points1) {
                for (const p2 of points2) {
                  const distance = Math.sqrt(
                    Math.pow(p1.x - p2.x, 2) +
                    Math.pow(p1.y - p2.y, 2) +
                    Math.pow(p1.z - p2.z, 2)
                  );

                  if (distance < minDist) {
                    minDist = distance;
                    bestPoint1 = p1;
                    bestPoint2 = p2;
                  }
                }
              }

              // 如果两点距离小于阈值，认为它们能连接，返回中点
              if (minDist < 100) { // 调整容差，单位mm
                return {
                  x: (bestPoint1.x + bestPoint2.x) / 2,
                  y: (bestPoint1.y + bestPoint2.y) / 2,
                  z: (bestPoint1.z + bestPoint2.z) / 2
                };
              }

              // 如果没有找到合适的连接点，返回null
              return null;
            }

            // 检查两个点是否相同
            function pointsEqual(p1, p2, tolerance = 1) {
              return Math.abs(p1.x - p2.x) < tolerance &&
                Math.abs(p1.y - p2.y) < tolerance &&
                Math.abs(p1.z - p2.z) < tolerance;
            }

            // 构建走线架连接图
            function buildCablewayGraph() {
              const graph = {};

              // 为每个走线架创建一个节点
              cableways.value.forEach(cw => {
                graph[cw.id] = [];
              });

              // 查找走线架之间的连接
              for (let i = 0; i < cableways.value.length; i++) {
                const cw1 = cableways.value[i];

                for (let j = i + 1; j < cableways.value.length; j++) {
                  const cw2 = cableways.value[j];

                  // 检查两个走线架是否相交
                  if (checkCablewaysIntersect(cw1, cw2)) {
                    // 双向连接
                    graph[cw1.id].push(cw2.id);
                    graph[cw2.id].push(cw1.id);
                  }
                }
              }

              return graph;
            }

            // 检查两个走线架是否相交
            function checkCablewaysIntersect(cw1, cw2) {
              // 计算两个走线架的边界
              const cw1MinX = Math.min(cw1.startPoint.x, cw1.endPoint.x) - cw1.width / 2;
              const cw1MaxX = Math.max(cw1.startPoint.x, cw1.endPoint.x) + cw1.width / 2;
              const cw1MinY = Math.min(cw1.startPoint.y, cw1.endPoint.y) - cw1.height / 2;
              const cw1MaxY = Math.max(cw1.startPoint.y, cw1.endPoint.y) + cw1.height / 2;
              const cw1MinZ = Math.min(cw1.startPoint.z, cw1.endPoint.z) - cw1.width / 2;
              const cw1MaxZ = Math.max(cw1.startPoint.z, cw1.endPoint.z) + cw1.width / 2;

              const cw2MinX = Math.min(cw2.startPoint.x, cw2.endPoint.x) - cw2.width / 2;
              const cw2MaxX = Math.max(cw2.startPoint.x, cw2.endPoint.x) + cw2.width / 2;
              const cw2MinY = Math.min(cw2.startPoint.y, cw2.endPoint.y) - cw2.height / 2;
              const cw2MaxY = Math.max(cw2.startPoint.y, cw2.endPoint.y) + cw2.height / 2;
              const cw2MinZ = Math.min(cw2.startPoint.z, cw2.endPoint.z) - cw2.width / 2;
              const cw2MaxZ = Math.max(cw2.startPoint.z, cw2.endPoint.z) + cw2.width / 2;

              // 检查是否有重叠
              const intersect = (
                cw1MaxX >= cw2MinX && cw1MinX <= cw2MaxX &&
                cw1MaxY >= cw2MinY && cw1MinY <= cw2MaxY &&
                cw1MaxZ >= cw2MinZ && cw1MinZ <= cw2MaxZ
              );

              return intersect;
            }

            // 查找两个走线架的交叉点
            function findCablewayIntersection(cablewayId1, cablewayId2) {
              const cw1 = cableways.value.find(cw => cw.id === cablewayId1);
              const cw2 = cableways.value.find(cw => cw.id === cablewayId2);

              if (!cw1 || !cw2) return null;

              // 如果走线架不相交，返回null
              if (!checkCablewaysIntersect(cw1, cw2)) return null;

              // 计算两个走线架的中点
              const midPoint1 = {
                x: (cw1.startPoint.x + cw1.endPoint.x) / 2,
                y: (cw1.startPoint.y + cw1.endPoint.y) / 2,
                z: (cw1.startPoint.z + cw1.endPoint.z) / 2
              };

              const midPoint2 = {
                x: (cw2.startPoint.x + cw2.endPoint.x) / 2,
                y: (cw2.startPoint.y + cw2.endPoint.y) / 2,
                z: (cw2.startPoint.z + cw2.endPoint.z) / 2
              };

              // 检查Y轴高度是否接近
              const sameHeight = Math.abs(midPoint1.y - midPoint2.y) < 50;

              if (sameHeight) {
                // 如果高度接近，计算中心点
                return {
                  x: (midPoint1.x + midPoint2.x) / 2,
                  y: (midPoint1.y + midPoint2.y) / 2,
                  z: (midPoint1.z + midPoint2.z) / 2
                };
              } else {
                // 如果高度不同，使用直线相交算法
                // 简化：返回XZ平面上的交点，Y取两个走线架的平均值
                // 计算中点
                return {
                  x: (midPoint1.x + midPoint2.x) / 2,
                  y: (midPoint1.y + midPoint2.y) / 2,
                  z: (midPoint1.z + midPoint2.z) / 2
                };
              }
            }

            // 使用BFS查找走线架网络中的最短路径
            function findShortestPath(graph, start, end) {
              // 如果起点或终点不在图中，返回空
              if (!graph[start] || !graph[end]) return [];

              // 如果起点就是终点，返回自身
              if (start === end) return [start];

              // 队列，用于BFS
              const queue = [[start]];
              // 已访问节点
              const visited = new Set([start]);

              while (queue.length > 0) {
                const path = queue.shift();
                const node = path[path.length - 1];

                // 检查相邻节点
                for (const neighbor of graph[node]) {
                  // 如果找到了终点，返回完整路径
                  if (neighbor === end) {
                    return [...path, end];
                  }

                  // 如果这个节点没有被访问过，加入队列
                  if (!visited.has(neighbor)) {
                    visited.add(neighbor);
                    queue.push([...path, neighbor]);
                  }
                }
              }

              // 如果找不到路径，返回空数组
              return [];
            }

            // 查找机柜上方的走线架点
            function findCablewayAboveCabinet(cabinet) {
              // 计算目标坐标（直接在机柜上方）
              const cabinetHeight = cabinet.u * config.cabinet.uHeight;
              const targetY = cabinet.y + cabinetHeight; // 机柜顶部

              // 找出所有Y坐标高于机柜顶部的走线架
              const candidates = [];

              for (const cw of cableways.value) {
                // 检查走线架高度是否高于机柜顶部
                if (cw.startPoint.y < targetY && cw.endPoint.y < targetY) {
                  continue; // 走线架低于机柜顶部，跳过
                }

                // 添加候选走线架及其连接点
                cw.connectionPoints.forEach(point => {
                  // 确保连接点高于机柜顶部
                  if (point.y < targetY) return;

                  // 计算连接点与机柜中心点的水平距离
                  const horizontalDistance = Math.sqrt(
                    Math.pow(point.x - cabinet.x, 2) +
                    Math.pow(point.z - cabinet.z, 2)
                  );

                  candidates.push({
                    point: point,
                    cablewayId: cw.id,
                    distance: horizontalDistance,
                    height: point.y - targetY // 高度差
                  });
                });
              }

              // 如果没有找到任何候选走线架，返回null
              if (candidates.length === 0) {
                return null;
              }

              // 优先考虑接近机柜中心点的连接点（水平距离较小的）
              candidates.sort((a, b) => a.distance - b.distance);

              // 返回最近的走线架点
              return {
                point: candidates[0].point,
                cablewayId: candidates[0].cablewayId
              };
            }

            // 寻找离特定点最近的走线架点
            function findNearestCablewayPoint(point) {
              let nearestPoint = null;
              let minDistance = Infinity;
              let cablewayId = null;

              for (const cw of cableways.value) {
                // 垂直投影到走线架高度
                const projectionPoint = {
                  x: point.x,
                  y: (cw.startPoint.y + cw.endPoint.y) / 2,
                  z: point.z
                };

                // 计算投影点到走线架中心线的水平距离
                const cablewayLine = {
                  startX: cw.startPoint.x,
                  startZ: cw.startPoint.z,
                  endX: cw.endPoint.x,
                  endZ: cw.endPoint.z
                };

                // 计算投影点到走线架中心线的水平距离
                const distance = distancePointToLine(
                  point.x, point.z,
                  cablewayLine.startX, cablewayLine.startZ,
                  cablewayLine.endX, cablewayLine.endZ
                );

                // 如果投影在走线架的宽度范围内，并且是最近的，则选择它
                if (distance <= cw.width / 2 && distance < minDistance) {
                  minDistance = distance;
                  nearestPoint = projectionPoint;
                  cablewayId = cw.id;
                }
              }

              if (nearestPoint) {
                return {
                  point: nearestPoint,
                  cablewayId: cablewayId
                };
              }

              return null;
            }

            // 计算点到线段的距离
            function distancePointToLine(px, pz, x1, z1, x2, z2) {
              const A = px - x1;
              const B = pz - z1;
              const C = x2 - x1;
              const D = z2 - z1;

              const dot = A * C + B * D;
              const lenSq = C * C + D * D;
              let param = -1;

              if (lenSq !== 0) {
                param = dot / lenSq;
              }

              let xx, zz;

              if (param < 0) {
                xx = x1;
                zz = z1;
              } else if (param > 1) {
                xx = x2;
                zz = z2;
              } else {
                xx = x1 + param * C;
                zz = z1 + param * D;
              }

              const dx = px - xx;
              const dz = pz - zz;

              return Math.sqrt(dx * dx + dz * dz);
            }

            // 添加线缆
            function addCable() {
              if (!cableFrom.value || !cableTo.value || cableFrom.value === cableTo.value) return;

              // 计算路径
              const result = calculateCablePath(cableFrom.value, cableTo.value);

              // 如果路径为空，表示无法连接，显示更明确的错误信息
              if (!result.path || result.path.length === 0) {
                alert('无法创建线缆连接: 未找到有效的走线架路径。\n请确保设备上方有走线架，并且走线架之间正确连接。');
                return;
              }

              // 检查路径是否符合垂直进出走线架的规则
              if (result.path.length >= 3) {
                // 检查起点到第一个走线架点的垂直性
                const firstSegmentVertical = Math.abs(result.path[0].x - result.path[1].x) < 10 &&
                  Math.abs(result.path[0].z - result.path[1].z) < 10;

                // 检查最后一个走线架点到终点的垂直性
                const lastSegmentVertical = Math.abs(result.path[result.path.length - 2].x - result.path[result.path.length - 1].x) < 10 &&
                  Math.abs(result.path[result.path.length - 2].z - result.path[result.path.length - 1].z) < 10;

                if (!firstSegmentVertical || !lastSegmentVertical) {
                  alert('线缆必须垂直进入和离开走线架！当前路径不符合要求。');
                  return;
                }
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              cables.value.push({
                id: 'cable-' + Date.now() + Math.random(),
                from: cableFrom.value,
                to: cableTo.value,
                path: result.path,
                length: result.length
              });
              cableFrom.value = '';
              cableTo.value = '';
              renderScene();
            }

            // 删除线缆
            function deleteCable(id) {
              // 保存当前状态以便撤销
              saveStateToHistory();

              cables.value = cables.value.filter(cable => cable.id !== id);
              if (selectedCable.value && selectedCable.value.id === id) {
                selectedCable.value = null;
              }
            }

            function dist(a, b) {
              return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2);
            }

            // 3D渲染
            function renderScene() {
              if (!scene) return;
              // 清空
              while (scene.children.length > 0) { scene.remove(scene.children[0]); }

              // 添加地板和网格 - 匹配参考代码中的样式
              // 地板
              const floorGeometry = new THREE.PlaneGeometry(50000, 50000);
              const floorMaterial = new THREE.MeshStandardMaterial({
                color: 0x2a2e3d,
                roughness: 0.7,
                metalness: 0.3,
                side: THREE.DoubleSide
              });
              const floor = new THREE.Mesh(floorGeometry, floorMaterial);
              floor.rotation.x = -Math.PI / 2;
              floor.receiveShadow = true;
              scene.add(floor);

              // 网格参考线
              const gridHelper = new THREE.GridHelper(20000, 100, 0x6e738a, 0x41465e);
              gridHelper.position.y = 0.2; // 略微抬高以避免z-fighting
              scene.add(gridHelper);

              // 添加坐标轴
              const axesHelper = new THREE.AxesHelper(500);
              scene.add(axesHelper);

              // 坐标系参考 - 自定义彩色轴线以匹配图像中的风格
              // 创建自定义的坐标轴而不是使用AxesHelper
              const axisLength = 600;

              // X轴 - 橙色
              const xAxisGeo = new THREE.BufferGeometry().setFromPoints([
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(axisLength, 0, 0)
              ]);
              const xAxisMat = new THREE.LineBasicMaterial({ color: 0xff9900 });
              const xAxis = new THREE.Line(xAxisGeo, xAxisMat);
              scene.add(xAxis);

              // Y轴 - 黄绿色
              const yAxisGeo = new THREE.BufferGeometry().setFromPoints([
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(0, axisLength, 0)
              ]);
              const yAxisMat = new THREE.LineBasicMaterial({ color: 0xccff00 });
              const yAxis = new THREE.Line(yAxisGeo, yAxisMat);
              scene.add(yAxis);

              // Z轴 - 蓝色
              const zAxisGeo = new THREE.BufferGeometry().setFromPoints([
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(0, 0, axisLength)
              ]);
              const zAxisMat = new THREE.LineBasicMaterial({ color: 0x00aaff });
              const zAxis = new THREE.Line(zAxisGeo, zAxisMat);
              scene.add(zAxis);

              // 添加坐标轴标签
              const axisLabels = [
                { text: "X", position: new THREE.Vector3(axisLength + 50, 0, 0), color: "#ff9900" },
                { text: "Y", position: new THREE.Vector3(0, axisLength + 50, 0), color: "#ccff00" },
                { text: "Z", position: new THREE.Vector3(0, 0, axisLength + 50), color: "#00aaff" }
              ];

              axisLabels.forEach(label => {
                const sprite = makeTextSprite(label.text, {
                  fontsize: 35,
                  textColor: label.color
                });
                sprite.position.copy(label.position);
                scene.add(sprite);
              });

              // 坐标方向指示器 - 在右上角显示XYZ方向
              const indicatorSize = 80;
              const indicatorMargin = 100;
              const indicatorPosition = new THREE.Vector3(
                window.innerWidth - indicatorSize - indicatorMargin,
                indicatorSize + indicatorMargin,
                0
              );

              const indicatorAxes = new THREE.AxesHelper(indicatorSize);
              // 创建一个特殊的正交相机用于UI元素
              const orthoCamera = new THREE.OrthographicCamera(
                -window.innerWidth / 2, window.innerWidth / 2,
                window.innerHeight / 2, -window.innerHeight / 2,
                0.1, 1000
              );

              // 存储作为场景UI的对象，避免在场景刷新时被移除
              window.axisIndicator = {
                axes: indicatorAxes,
                camera: orthoCamera,
                labels: []
              };

              // 添加坐标系指示器的文本标签
              ["X", "Y", "Z"].forEach((text, i) => {
                const colors = ["#ff9900", "#ccff00", "#00aaff"]; // 匹配主坐标轴的颜色
                const positions = [
                  new THREE.Vector3(indicatorSize + 5, 0, 0),
                  new THREE.Vector3(0, indicatorSize + 5, 0),
                  new THREE.Vector3(0, 0, indicatorSize + 5)
                ];

                const sprite = makeTextSprite(text, {
                  fontsize: 24,
                  textColor: colors[i]
                });
                sprite.position.copy(positions[i]);
                window.axisIndicator.labels.push(sprite);
              });

              // 渲染机柜
              renderCabinets();

              // 设备
              devices.value.forEach(dev => {
                const cab = cabinets.value.find(c => c.id === dev.cabinetId);
                if (!cab) return;

                // 检查机柜是否旋转
                const cabinetRotation = cab.rotation || 0;
                const isRotated = cabinetRotation === 90;

                // 支持多U高度的设备
                const deviceWidth = config.device.defaultWidth;
                const deviceHeight = (dev.height || 1) * config.cabinet.uHeight; // 使用设备高度（U数）
                const deviceDepth = config.device.defaultDepth;

                // 如果机柜旋转了，设备的宽度和深度也要相应调整
                const actualDeviceWidth = isRotated ? deviceDepth : deviceWidth;
                const actualDeviceDepth = isRotated ? deviceWidth : deviceDepth;

                // 使用更现代的材质渲染设备
                const deviceGeometry = new THREE.BoxGeometry(actualDeviceWidth, deviceHeight, actualDeviceDepth);
                const deviceMaterial = new THREE.MeshStandardMaterial({
                  color: 0x333333, // 深灰色
                  metalness: 0.7,
                  roughness: 0.2
                });
                const mesh = new THREE.Mesh(deviceGeometry, deviceMaterial);

                // 计算设备中心点位置 - 考虑多U高度
                const deviceCenterU = dev.u + (dev.height - 1) / 2;
                mesh.position.set(cab.x, deviceCenterU * config.cabinet.uHeight + 22 + cab.y, cab.z);
                mesh.castShadow = true;
                mesh.receiveShadow = true;

                // 添加设备边框 - 使线框更明显
                const edgesGeometry = new THREE.EdgesGeometry(deviceGeometry);
                const edgesMaterial = new THREE.LineBasicMaterial({ color: 0xaaaaaa });
                const wireframe = new THREE.LineSegments(edgesGeometry, edgesMaterial);
                mesh.add(wireframe);

                scene.add(mesh);

                // 在设备上添加设备名称标签 - 模拟真实机架中的设备标签
                if (showLabels.value) {
                  // 创建设备标签面板 - 黑色背景，白色文字
                  const deviceLabelWidth = actualDeviceWidth - 10;
                  const deviceLabelHeight = 30;
                  const deviceLabelDepth = 5;
                  const deviceLabelGeo = new THREE.BoxGeometry(deviceLabelWidth, deviceLabelHeight, deviceLabelDepth);
                  const deviceLabelMat = new THREE.MeshPhongMaterial({
                    color: 0x000000,
                    transparent: true,
                    opacity: 0.9,
                    side: THREE.DoubleSide
                  });

                  const deviceLabelMesh = new THREE.Mesh(deviceLabelGeo, deviceLabelMat);

                  // 将标签放置在设备中间上方，考虑机柜旋转
                  if (isRotated) {
                    deviceLabelMesh.position.set(
                      cab.x + actualDeviceWidth / 2 + deviceLabelDepth / 2,
                      deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                      cab.z
                    );
                    deviceLabelMesh.rotation.y = Math.PI / 2; // 旋转90度，匹配设备方向
                  } else {
                    deviceLabelMesh.position.set(
                      cab.x,
                      deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                      cab.z + actualDeviceDepth / 2 + deviceLabelDepth / 2
                    );
                  }
                  scene.add(deviceLabelMesh);

                  // 添加设备标签文字
                  const deviceLabelText = makeTextSprite(dev.name, {
                    fontsize: 22,
                    textColor: "white",
                    backgroundColor: null, // 透明背景
                    borderThickness: 0
                  });

                  // 将文字标签放在面板前面
                  if (isRotated) {
                    deviceLabelText.position.set(
                      cab.x + actualDeviceWidth / 2 + deviceLabelDepth + 1,
                      deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                      cab.z
                    );
                  } else {
                    deviceLabelText.position.set(
                      cab.x,
                      deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                      cab.z + actualDeviceDepth / 2 + deviceLabelDepth + 1
                    );
                  }
                  deviceLabelText.userData.isLabel = true;
                  scene.add(deviceLabelText);

                  // 添加U位信息标签 - 显示在设备标签下方
                  const uInfoText = "U" + dev.u + (dev.height > 1 ? "-" + (dev.u + dev.height - 1) : "");
                  const uPosLabel = makeTextSprite(uInfoText, {
                    fontsize: 16,
                    textColor: "white",
                    backgroundColor: null
                  });

                  // 放置U位标签
                  if (isRotated) {
                    uPosLabel.position.set(
                      cab.x + actualDeviceWidth / 2 + deviceLabelDepth + 1,
                      deviceCenterU * config.cabinet.uHeight + 22 - 20 + cab.y,
                      cab.z
                    );
                  } else {
                    uPosLabel.position.set(
                      cab.x,
                      deviceCenterU * config.cabinet.uHeight + 22 - 20 + cab.y,
                      cab.z + actualDeviceDepth / 2 + deviceLabelDepth + 1
                    );
                  }
                  uPosLabel.userData.isLabel = true;
                  scene.add(uPosLabel);
                }

                // 渲染前面板外形 - 突出显示设备前面板，使用更深的黑色
                const frontPanelGeo = new THREE.PlaneGeometry(
                  isRotated ? actualDeviceWidth - 10 : actualDeviceWidth - 10,
                  deviceHeight - 10
                );
                const frontPanelMat = new THREE.MeshPhongMaterial({
                  color: 0x050505, // 近乎纯黑色
                  specular: 0x222222,
                  shininess: 20,
                  side: THREE.DoubleSide
                });
                const frontPanel = new THREE.Mesh(frontPanelGeo, frontPanelMat);

                // 根据机柜旋转调整前面板位置和旋转
                if (isRotated) {
                  // 当机柜旋转时，前面板应该面向X轴正向
                  frontPanel.position.set(
                    cab.x + actualDeviceWidth / 2 + 1,
                    deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                    cab.z
                  );
                  frontPanel.rotation.y = Math.PI / 2; // 旋转90度
                } else {
                  // 正常情况，前面板面向Z轴正向
                  frontPanel.position.set(
                    cab.x,
                    deviceCenterU * config.cabinet.uHeight + 22 + cab.y,
                    cab.z + actualDeviceDepth / 2 + 1
                  );
                }
                scene.add(frontPanel);

                // 端口 - 水平排列在设备前面板上 - 使用蓝色圆柱形端口，类似参考代码风格
                dev.ports.forEach((p, idx) => {
                  // 使用圆柱体模拟网络端口形状
                  const portRadius = 8;
                  const portHeight = 15;
                  const portGeo = new THREE.CylinderGeometry(portRadius, portRadius, portHeight, 16);

                  // 使用带有发光效果的材质
                  const portMat = new THREE.MeshStandardMaterial({
                    color: 0x0088ff,
                    metalness: 0.8,
                    roughness: 0.2,
                    emissive: new THREE.Color(0x0088ff).multiplyScalar(0.5)
                  });

                  const portMesh = new THREE.Mesh(portGeo, portMat);

                  // 端口位于设备前方，水平排列，考虑机柜旋转
                  const portPos = getPortPos(dev, idx);

                  // 需要旋转圆柱体，使其朝向正确的方向
                  if (isRotated) {
                    portMesh.rotation.z = Math.PI / 2;
                    // 调整端口位置，使其稍微突出设备
                    portMesh.position.set(
                      portPos.x + portHeight / 2,
                      portPos.y,
                      portPos.z
                    );
                  } else {
                    portMesh.rotation.x = Math.PI / 2;
                    // 调整端口位置，使其稍微突出设备
                    portMesh.position.set(
                      portPos.x,
                      portPos.y,
                      portPos.z + portHeight / 2
                    );
                  }

                  scene.add(portMesh);

                  // 如果显示标签选项已启用，添加端口号标签
                  if (showLabels.value && dev.ports.length <= 12) { // 只有少量端口时才显示标签，避免过度拥挤
                    const portLabel = makeTextSprite((idx + 1).toString(), {
                      fontsize: 14,
                      textColor: "white",
                      backgroundColor: null
                    });

                    // 根据机柜旋转调整端口标签位置
                    const labelOffsetX = isRotated ? 20 : 0;
                    const labelOffsetZ = isRotated ? 0 : 20;

                    portLabel.position.set(
                      portPos.x + labelOffsetX,
                      portPos.y + 15,
                      portPos.z + labelOffsetZ
                    );
                    portLabel.userData.isLabel = true;
                    scene.add(portLabel);
                  }
                });
              });
              // 走线架
              if (showCableways.value) {
                cableways.value.forEach(cw => {
                  const startPoint = cw.startPoint;
                  const endPoint = cw.endPoint;

                  // 基于类型创建不同的走线架形状
                  if (cw.type === 'linear') {
                    // 直线走线架
                    renderLinearCableway(cw);
                  } else if (cw.type === 'cross') {
                    // 十字走线架
                    renderCrossCableway(cw);
                  } else if (cw.type === 'tshape') {
                    // T型走线架
                    renderTShapeCableway(cw);
                  } else if (cw.type === 'corner') {
                    // 弯角走线架
                    renderCornerCableway(cw);
                  }

                  // 渲染连接点 - 只在显示连接点选项开启时渲染
                  if (showConnectors.value) {
                    cw.connectionPoints.forEach(point => {
                      const connGeo = new THREE.SphereGeometry(config.cableway.connectionRadius, 8, 8);
                      const connMat = new THREE.MeshLambertMaterial({ color: 0xff0000 });
                      const connMesh = new THREE.Mesh(connGeo, connMat);
                      connMesh.position.set(point.x, point.y, point.z);
                      connMesh.userData.isConnector = true;
                      scene.add(connMesh);
                    });
                  }

                  // 添加走线架标签
                  const cablewayLabel = makeTextSprite(cw.name, {
                    fontsize: 22,
                    textColor: "black",
                    backgroundColor: "rgba(255,200,150,0.8)",
                    borderColor: "rgba(150,100,50,0.8)",
                    borderThickness: 2
                  });
                  const centerX = (startPoint.x + endPoint.x) / 2;
                  const centerY = (startPoint.y + endPoint.y) / 2;
                  const centerZ = (startPoint.z + endPoint.z) / 2;
                  cablewayLabel.position.set(centerX, centerY + 30, centerZ);
                  cablewayLabel.userData.isLabel = true;
                  scene.add(cablewayLabel);
                });
              }

              // 简单走线架渲染
              patchPanels.value.forEach(pp => {
                const patchWidth = pp.width || 800;
                const patchDepth = pp.depth || 300;

                const geo = new THREE.BoxGeometry(patchWidth, 20, patchDepth);
                const mat = new THREE.MeshLambertMaterial({ color: 0xff8800 });
                const mesh = new THREE.Mesh(geo, mat);
                mesh.position.set(pp.pos.x, pp.pos.y, pp.pos.z);
                scene.add(mesh);

                // 走线架标签
                const ppLabel = pp.type === 'cabinet'
                  ? '走线架(' + getCabinetName(pp.cabinetId) + ')'
                  : '全局走线架';
                const sprite = makeTextSprite(ppLabel);
                sprite.position.set(pp.pos.x, pp.pos.y + 30, pp.pos.z);
                sprite.userData.isLabel = true;
                scene.add(sprite);
              });

              // 线缆 - 使用优化的渲染方法
              renderCables();

              // 灯光 - 更新为匹配参考代码中的照明设置
              // 环境光 - 基础照明
              const ambientLight = new THREE.AmbientLight(0x656565);
              scene.add(ambientLight);

              // 主方向光 - 带阴影
              const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
              directionalLight.position.set(5, 12, 7.5);
              directionalLight.castShadow = true;
              directionalLight.shadow.mapSize.width = 2048;
              directionalLight.shadow.mapSize.height = 2048;
              directionalLight.shadow.camera.near = 0.5;
              directionalLight.shadow.camera.far = 50;
              directionalLight.shadow.bias = -0.0001;
              scene.add(directionalLight);

              // 半球光 - 提供更自然的环境光照
              const hemisphereLight = new THREE.HemisphereLight(0x7c849b, 0x383d51, 0.7);
              scene.add(hemisphereLight);

              // 渲染Waypoint
              waypoints.value.forEach(wp => {
                // 创建Waypoint标记 - 使用小球体
                const wpGeo = new THREE.SphereGeometry(30, 16, 16);
                const wpMat = new THREE.MeshLambertMaterial({ color: 0xff6600 });
                const wpMesh = new THREE.Mesh(wpGeo, wpMat);
                wpMesh.position.set(wp.x, wp.y, wp.z);
                scene.add(wpMesh);

                // 添加Waypoint标签
                if (showLabels.value) {
                  const wpLabel = makeTextSprite(wp.name, {
                    fontsize: 24,
                    textColor: "black",
                    backgroundColor: "rgba(255,180,100,0.8)",
                    borderColor: "rgba(200,100,50,0.8)",
                    borderThickness: 2
                  });
                  wpLabel.position.set(wp.x, wp.y + 50, wp.z);
                  wpLabel.userData.isLabel = true;
                  scene.add(wpLabel);
                }
              });

              renderer.render(scene, camera);
            }

            // 渲染直线走线架
            function renderLinearCableway(cw) {
              const startPoint = cw.startPoint;
              const endPoint = cw.endPoint;

              // 创建走线架体
              const length = dist(startPoint, endPoint);
              const direction = new THREE.Vector3(
                endPoint.x - startPoint.x,
                endPoint.y - startPoint.y,
                endPoint.z - startPoint.z
              ).normalize();

              // 创建一个长方体走线架
              const geo = new THREE.BoxGeometry(cw.width, cw.height, length);
              const mat = new THREE.MeshLambertMaterial({ color: 0xddaa66, transparent: true, opacity: 0.7 });
              const mesh = new THREE.Mesh(geo, mat);

              // 设置中点位置
              const midpoint = {
                x: (startPoint.x + endPoint.x) / 2,
                y: (startPoint.y + endPoint.y) / 2,
                z: (startPoint.z + endPoint.z) / 2
              };
              mesh.position.set(midpoint.x, midpoint.y, midpoint.z);

              // 旋转走线架以面向正确的方向
              if (direction.z !== 0 || direction.x !== 0) {
                mesh.lookAt(new THREE.Vector3(
                  startPoint.x + direction.x,
                  startPoint.y + direction.y,
                  startPoint.z + direction.z
                ));
              }

              scene.add(mesh);

              // 添加走线架边框线
              const edges = new THREE.EdgesGeometry(geo);
              const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x996633 }));
              line.position.copy(mesh.position);
              line.rotation.copy(mesh.rotation);
              scene.add(line);

              // 如果显示标签选项已启用，添加走线架标签
              if (showLabels.value) {
                const cablewayLabel = makeTextSprite(cw.name, {
                  fontsize: 22,
                  textColor: "black",
                  backgroundColor: "rgba(255,200,150,0.8)",
                  borderColor: "rgba(150,100,50,0.8)",
                  borderThickness: 2
                });
                cablewayLabel.position.set(midpoint.x, midpoint.y + 30, midpoint.z);
                cablewayLabel.userData.isLabel = true;
                scene.add(cablewayLabel);
              }

              // 如果显示连接点选项已启用，渲染连接点
              if (showConnectors.value) {
                cw.connectionPoints.forEach(point => {
                  const connGeo = new THREE.SphereGeometry(config.cableway.connectionRadius, 8, 8);
                  const connMat = new THREE.MeshLambertMaterial({ color: 0xff0000 });
                  const connMesh = new THREE.Mesh(connGeo, connMat);
                  connMesh.position.set(point.x, point.y, point.z);
                  connMesh.userData.isConnector = true;
                  scene.add(connMesh);
                });
              }
            }

            // 渲染十字走线架
            function renderCrossCableway(cw) {
              const startPoint = cw.startPoint;
              const endPoint = cw.endPoint;

              // 创建主要走线架段落（水平部分）
              renderLinearCableway({
                ...cw,
                startPoint: startPoint,
                endPoint: endPoint
              });

              // 创建垂直部分
              const centerX = (startPoint.x + endPoint.x) / 2;
              const centerY = (startPoint.y + endPoint.y) / 2;
              const centerZ = (startPoint.z + endPoint.z) / 2;

              const vertLength = Math.abs(endPoint.z - startPoint.z) * 2;
              const vertStart = {
                x: centerX,
                y: centerY,
                z: centerZ - vertLength / 2
              };
              const vertEnd = {
                x: centerX,
                y: centerY,
                z: centerZ + vertLength / 2
              };

              renderLinearCableway({
                ...cw,
                startPoint: vertStart,
                endPoint: vertEnd
              });
            }

            // 渲染T型走线架
            function renderTShapeCableway(cw) {
              const startPoint = cw.startPoint;
              const endPoint = cw.endPoint;

              // 创建主要走线架段落（水平部分）
              renderLinearCableway({
                ...cw,
                startPoint: startPoint,
                endPoint: endPoint
              });

              // 创建垂直部分
              const centerX = (startPoint.x + endPoint.x) / 2;
              const centerY = (startPoint.y + endPoint.y) / 2;
              const centerZ = (startPoint.z + endPoint.z) / 2;

              const vertLength = Math.abs(endPoint.z - startPoint.z);
              const vertStart = {
                x: centerX,
                y: centerY,
                z: centerZ
              };
              const vertEnd = {
                x: centerX,
                y: centerY,
                z: centerZ - vertLength
              };

              renderLinearCableway({
                ...cw,
                startPoint: vertStart,
                endPoint: vertEnd
              });
            }

            // 渲染弯角走线架
            function renderCornerCableway(cw) {
              const startPoint = cw.startPoint;
              const endPoint = cw.endPoint;

              // 创建弯角点
              const cornerPoint = {
                x: startPoint.x,
                y: startPoint.y,
                z: endPoint.z
              };

              // 水平段
              renderLinearCableway({
                ...cw,
                startPoint: startPoint,
                endPoint: cornerPoint
              });

              // 垂直段
              renderLinearCableway({
                ...cw,
                startPoint: cornerPoint,
                endPoint: endPoint
              });
            }

            function makeTextSprite(message, style) {
              // 设置默认样式
              style = style || {
                fontsize: 24,
                textColor: "black",
                backgroundColor: "rgba(255,255,255,0.7)", // 半透明白色背景
                borderColor: "rgba(0,0,0,0.5)",         // 边框颜色
                borderThickness: 2,                     // 边框厚度
                padding: 6                              // 内边距
              };

              const canvas = document.createElement('canvas');
              const ctx = canvas.getContext('2d');

              // 设置字体
              const fontSize = style.fontsize || 24;
              ctx.font = fontSize + "px Arial";

              // 测量文本宽度
              const textWidth = ctx.measureText(message).width;
              const width = textWidth + (style.padding || 6) * 2;
              const height = fontSize + (style.padding || 6) * 2;

              // 调整画布尺寸
              canvas.width = width;
              canvas.height = height;

              // 重新设置字体（因为画布大小改变后上下文会重置）
              ctx.font = fontSize + "px Arial";

              // 绘制背景和边框
              if (style.backgroundColor) {
                ctx.fillStyle = style.backgroundColor;
                ctx.fillRect(0, 0, width, height);
              }

              if (style.borderColor && style.borderThickness) {
                ctx.strokeStyle = style.borderColor;
                ctx.lineWidth = style.borderThickness;
                ctx.strokeRect(0, 0, width, height);
              }

              // 绘制文本
              ctx.fillStyle = style.textColor || "black";
              ctx.textAlign = "center";
              ctx.textBaseline = "middle";
              ctx.fillText(message, width / 2, height / 2);

              const texture = new THREE.CanvasTexture(canvas);
              const spriteMat = new THREE.SpriteMaterial({ map: texture });
              const sprite = new THREE.Sprite(spriteMat);

              // 设置精灵缩放比例
              // 保持高宽比，根据文本长度自动调整宽度
              const aspectRatio = width / height;
              sprite.scale.set(aspectRatio * 100, 100, 1);

              return sprite;
            }

            // 更新选中的机柜
            function updateSelectedCabinet() {
              // 检查名称是否与其他机柜冲突
              const duplicateCabinet = cabinets.value.find(cab =>
                cab.name === selectedCabinet.value.name && cab.id !== selectedCabinet.value.id
              );

              if (duplicateCabinet) {
                alert('已存在同名机柜，请使用其他名称');
                // 恢复原始名称
                const originalCabinet = cabinets.value.find(cab => cab.id === selectedCabinet.value.id);
                if (originalCabinet) {
                  selectedCabinet.value.name = originalCabinet.name;
                }
                return;
              }

              // 保存当前状态用于撤销
              saveStateToHistory();

              // 更新机柜数据
              const index = cabinets.value.findIndex(cab => cab.id === selectedCabinet.value.id);
              if (index !== -1) {
                cabinets.value[index] = { ...selectedCabinet.value };
              }

              // 更新3D视图
              renderScene();
            }

            // 删除机柜并关闭属性面板
            function deleteCabinetAndClose(cabinetId) {
              if (confirm('确定要删除此机柜吗？')) {
                deleteCabinet(cabinetId);
                selectedCabinet.value = null;
              }
            }

            // 线缆和机柜点击
            function onCanvasClick(event) {
              if (!renderer || !scene || !camera) return;
              const rect = renderer.domElement.getBoundingClientRect();
              mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
              mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
              raycaster.setFromCamera(mouse, camera);
              const intersects = raycaster.intersectObjects(scene.children, true);

              console.log("点击检测，获取到碰撞物体数：", intersects.length);
              
              // 首先检查是否点击了机柜
              for (let i = 0; i < intersects.length; i++) {
                const obj = intersects[i].object;
                console.log("检查物体：", obj.userData);
                
                if (obj.userData && obj.userData.isCabinet) {
                  const cabinetId = obj.userData.cabinetId || obj.userData.parentRackId;
                  console.log("点击到机柜，ID：", cabinetId);
                  
                  const cabinet = cabinets.value.find(cab => cab.id === cabinetId);
                  if (cabinet) {
                    console.log("找到机柜数据：", cabinet);
                    // 创建一个深拷贝以便编辑
                    selectedCabinet.value = JSON.parse(JSON.stringify(cabinet));
                    // 关闭其他对话框
                    selectedCable.value = null;
                    
                    // 高亮显示选中的机柜
                    highlightSelectedCabinet(cabinetId);
                    
                    return;
                  }
                }
              }

              // 然后检查是否点击了线缆
              for (let i = 0; i < intersects.length; i++) {
                const obj = intersects[i].object;
                if (obj.userData.cableId) {
                  // 选择线缆
                  selectedCable.value = cables.value.find(c => c.id === obj.userData.cableId);
                  
                  // 关闭机柜属性面板
                  selectedCabinet.value = null;
                  return;
                }
              }

              // 如果点击了空白处，关闭所有面板
              selectedCable.value = null;
              selectedCabinet.value = null;
              // 清除任何高亮效果
              highlightSelectedCabinet(null);
            }

            // 初始化3D
            Vue.onMounted(() => {
              console.log("初始化3D场景...");
              // 保存初始空白状态用于撤销
              saveStateToHistory();

              scene = new THREE.Scene();
              scene.background = new THREE.Color(0x252934); // 设置深蓝灰色背景，与参考代码匹配
              camera = new THREE.PerspectiveCamera(60, window.innerWidth / (window.innerHeight), 1, 50000);

              // 恢复默认的Three.js相机设置
              theta = Math.PI / 4; // 初始角度
              phi = Math.PI / 4;   // 调整初始仰角，稍微俯视一点
              cameraDistance = 3000;
              cameraTarget = new THREE.Vector3(0, 0, 0); // 重置相机目标点

              // 使用球坐标系设置初始相机位置
              camera.position.x = cameraDistance * Math.sin(theta) * Math.cos(phi);
              camera.position.y = cameraDistance * Math.sin(phi);
              camera.position.z = cameraDistance * Math.cos(theta) * Math.cos(phi);
              camera.up.set(0, 1, 0); // Y轴向上
              camera.lookAt(cameraTarget);

              renderer = new THREE.WebGLRenderer({ antialias: true });
              renderer.setSize(window.innerWidth - 300, window.innerHeight);
              renderer.shadowMap.enabled = true; // 启用阴影渲染
              document.getElementById('three-canvas').appendChild(renderer.domElement);
              raycaster = new THREE.Raycaster();
              mouse = new THREE.Vector2();

              // 简化的控制
              let dragging = false;
              let lastX = 0, lastY = 0;

              // 更新摄像机位置
              updateCamera = () => {
                if (!camera) return; // 如果相机未初始化，直接返回

                // 使用球坐标系更新相机位置
                const previousDistToTarget = camera.position.distanceTo(cameraTarget);

                // 计算新的相机位置
                camera.position.set(
                  cameraTarget.x + cameraDistance * Math.sin(theta) * Math.cos(phi),
                  cameraTarget.y + cameraDistance * Math.sin(phi),
                  cameraTarget.z + cameraDistance * Math.cos(theta) * Math.cos(phi)
                );

                camera.lookAt(cameraTarget);

                if (renderer) {
                  renderer.render(scene, camera);
                }
              };

              // 鼠标按下
              renderer.domElement.addEventListener('mousedown', e => {
                if (e.button === 0) { // 左键 - 旋转
                  dragging = true;
                }
                lastX = e.clientX;
                lastY = e.clientY;
                e.preventDefault();
              });

              // 鼠标移动
              renderer.domElement.addEventListener('mousemove', e => {
                if (dragging) {
                  // 基本的轨道控制逻辑
                  const deltaX = e.clientX - lastX;
                  const deltaY = e.clientY - lastY;

                  theta -= deltaX * 0.01;
                  phi -= deltaY * 0.01;
                  phi = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, phi));

                  // 更新相机位置，相对于当前的目标点
                  camera.position.set(
                    cameraTarget.x + cameraDistance * Math.sin(theta) * Math.cos(phi),
                    cameraTarget.y + cameraDistance * Math.sin(phi),
                    cameraTarget.z + cameraDistance * Math.cos(theta) * Math.cos(phi)
                  );

                  camera.lookAt(cameraTarget);
                  renderer.render(scene, camera);
                }

                lastX = e.clientX;
                lastY = e.clientY;
              });

              // 鼠标释放
              window.addEventListener('mouseup', () => {
                dragging = false;
              });

              // 滚轮缩放
              renderer.domElement.addEventListener('wheel', e => {
                e.preventDefault();
                // 缩放相机距离 - 移除最小和最大距离限制，允许无限缩放
                // 增大缩放步长，放大系数与当前距离相关，让远距离缩放更快
                const zoomFactor = Math.max(1, cameraDistance / 2000);
                cameraDistance += e.deltaY * 5 * zoomFactor;

                // 更新相机位置
                camera.position.set(
                  cameraTarget.x + cameraDistance * Math.sin(theta) * Math.cos(phi),
                  cameraTarget.y + cameraDistance * Math.sin(phi),
                  cameraTarget.z + cameraDistance * Math.cos(theta) * Math.cos(phi)
                );

                camera.lookAt(cameraTarget);
                renderer.render(scene, camera);
              });

              // 禁用右键菜单
              renderer.domElement.addEventListener('contextmenu', e => {
                e.preventDefault();
              });

              // 键盘控制 - 监听键盘事件
              window.addEventListener('keydown', e => {
                // 检查是否有输入框被聚焦，如果有则不进行视图控制
                const activeElement = document.activeElement;
                const isInputFocused = activeElement.tagName === 'INPUT' ||
                  activeElement.tagName === 'TEXTAREA' ||
                  activeElement.tagName === 'SELECT';

                // 撤销/恢复快捷键 - 即使在输入框中也应允许
                if (e.ctrlKey || e.metaKey) {  // metaKey 用于 Mac 上的 Command 键
                  if (e.key === 'z' || e.key === 'Z') {
                    e.preventDefault();
                    undo();
                    return;
                  } else if (e.key === 'y' || e.key === 'Y') {
                    e.preventDefault();
                    redo();
                    return;
                  }
                }

                // 如果输入框被聚焦，不执行其他键盘控制
                if (isInputFocused) {
                  return;
                }

                const step = 100; // 每次移动的步长，单位为毫米

                switch (e.key) {
                  case 'ArrowLeft':
                    panCamera(-step, 0, 0);
                    break;
                  case 'ArrowRight':
                    panCamera(step, 0, 0);
                    break;
                  case 'ArrowUp':
                    panCamera(0, step, 0); // 上方向是y轴正方向
                    break;
                  case 'ArrowDown':
                    panCamera(0, -step, 0); // 下方向是y轴负方向
                    break;
                  // 添加视图切换快捷键
                  case '1': // 正视图 (Front view)
                    theta = 0;
                    phi = 0;
                    updateCamera();
                    break;
                  case '2': // 侧视图 (Side view)
                    theta = Math.PI / 2;
                    phi = 0;
                    updateCamera();
                    break;
                  case '3': // 顶视图 (Top view)
                    theta = 0;
                    phi = Math.PI / 2;
                    updateCamera();
                    break;
                  case '4': // 等轴测图 (Isometric view)
                    theta = Math.PI / 4;
                    phi = Math.PI / 4;
                    updateCamera();
                    break;
                }
              });

              // 窗口大小调整
              window.addEventListener('resize', () => {
                camera.aspect = (window.innerWidth - 300) / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth - 300, window.innerHeight);
                renderer.render(scene, camera);
              });

              renderer.domElement.addEventListener('click', onCanvasClick);
              console.log("已绑定点击事件处理器");

              renderScene();
            });

            // 响应式渲染
            Vue.watch([cabinets, devices, patchPanels, cables], renderScene, { deep: true });

            // 响应式渲染
            Vue.watch([cabinets, devices, patchPanels, cableways, cables], renderScene, { deep: true });

            // 监听UI控制选项
            Vue.watch(showLabels, renderScene);
            Vue.watch(showConnectors, renderScene);
            // 监听走线架控制选项
            Vue.watch(showCableways, renderScene);

            // 机柜渲染部分，删除顶部开孔和U位刻度
            function renderCabinets() {
              cabinets.value.forEach((cab) => {
                // 创建机柜框架而非实心盒子
                const cabinetWidth = cab.width || config.cabinet.defaultWidth;
                const cabinetHeight = cab.u * config.cabinet.uHeight;
                const cabinetDepth = cab.depth || config.cabinet.defaultDepth;

                // 考虑旋转角度
                const rotation = cab.rotation || 0; // 默认为0度，不旋转
                const isRotated = rotation === 90; // 是否旋转了90度

                // 如果旋转90度，交换宽度和深度
                const actualWidth = isRotated ? cabinetDepth : cabinetWidth;
                const actualDepth = isRotated ? cabinetWidth : cabinetDepth;

                // 创建机柜框架 - 参考代码中的样式
                const rackGroup = new THREE.Group();
                rackGroup.position.set(cab.x, cabinetHeight / 2 + cab.y, cab.z);
                rackGroup.userData.isCabinet = true;
                rackGroup.userData.cabinetId = cab.id;

                // 柱子宽度
                const pillarWidth = actualWidth * 0.05; // 约5%宽度用于支柱

                // 创建机柜支柱 - 四个角落的立柱
                // 前左
                createRackPillar(rackGroup, -actualWidth / 2 + pillarWidth / 2, 0, actualDepth / 2 - pillarWidth / 2, pillarWidth, cabinetHeight, pillarWidth);
                // 前右
                createRackPillar(rackGroup, actualWidth / 2 - pillarWidth / 2, 0, actualDepth / 2 - pillarWidth / 2, pillarWidth, cabinetHeight, pillarWidth);
                // 后左
                createRackPillar(rackGroup, -actualWidth / 2 + pillarWidth / 2, 0, -actualDepth / 2 + pillarWidth / 2, pillarWidth, cabinetHeight, pillarWidth);
                // 后右
                createRackPillar(rackGroup, actualWidth / 2 - pillarWidth / 2, 0, -actualDepth / 2 + pillarWidth / 2, pillarWidth, cabinetHeight, pillarWidth);

                // 创建机柜的顶部和底部框架
                createRackHorizontalBar(rackGroup, 0, cabinetHeight / 2 - pillarWidth / 2, 0, actualWidth, pillarWidth, actualDepth); // 顶部
                createRackHorizontalBar(rackGroup, 0, -cabinetHeight / 2 + pillarWidth / 2, 0, actualWidth, pillarWidth, actualDepth); // 底部

                // 创建左右实体侧挡板
                const sideColor = 0x2a2a2a;
                const sideOpacity = 0.85; // 更明显的不透明度
                const sidePanelThickness = 15; // 更厚的侧挡板

                // 左侧挡板
                createRackSidePanel(
                  rackGroup, 
                  -actualWidth / 2, 
                  0, 
                  0, 
                  sidePanelThickness, 
                  cabinetHeight, 
                  actualDepth, 
                  sideOpacity
                );
                
                // 右侧挡板
                createRackSidePanel(
                  rackGroup, 
                  actualWidth / 2, 
                  0, 
                  0, 
                  sidePanelThickness, 
                  cabinetHeight, 
                  actualDepth, 
                  sideOpacity
                );
                
                // 后挡板
                createRackSidePanel(
                  rackGroup,
                  0,
                  0,
                  -actualDepth / 2,
                  actualWidth,
                  cabinetHeight,
                  sidePanelThickness,
                  sideOpacity
                );

                scene.add(rackGroup);

                // 辅助函数 - 创建机柜支柱
                function createRackPillar(parent, x, y, z, width, height, depth) {
                  const geometry = new THREE.BoxGeometry(width, height, depth);
                  const material = new THREE.MeshStandardMaterial({
                    color: 0x2a2a2a,
                    metalness: 0.8,
                    roughness: 0.4,
                    emissive: 0x000000,
                    emissiveIntensity: 0
                  });

                  const pillar = new THREE.Mesh(geometry, material);
                  pillar.position.set(x, y, z);
                  pillar.castShadow = true;
                  pillar.receiveShadow = true;
                  pillar.userData = { type: 'rackComponent' };

                  parent.add(pillar);
                  return pillar;
                }

                // 辅助函数 - 创建机柜横杆
                function createRackHorizontalBar(parent, x, y, z, width, height, depth) {
                  const geometry = new THREE.BoxGeometry(width, height, depth);
                  const material = new THREE.MeshStandardMaterial({
                    color: 0x2a2a2a,
                    metalness: 0.8,
                    roughness: 0.4,
                    transparent: true,
                    opacity: 0.6,
                    emissive: 0x000000,
                    emissiveIntensity: 0
                  });

                  const bar = new THREE.Mesh(geometry, material);
                  bar.position.set(x, y, z);
                  bar.castShadow = true;
                  bar.receiveShadow = true;
                  bar.userData = { type: 'rackComponent' };

                  parent.add(bar);
                  return bar;
                }

                // 辅助函数 - 创建机柜侧面板
                function createRackSidePanel(parent, x, y, z, width, height, depth, opacity = 0.3) {
                  const geometry = new THREE.BoxGeometry(width, height, depth);
                  const material = new THREE.MeshStandardMaterial({
                    color: 0x2a2a2a,
                    metalness: 0.8,
                    roughness: 0.4,
                    transparent: true,
                    opacity: opacity,
                    emissive: 0x000000,
                    emissiveIntensity: 0
                  });

                  const panel = new THREE.Mesh(geometry, material);
                  panel.position.set(x, y, z);
                  panel.castShadow = true;
                  panel.receiveShadow = true;
                  panel.userData = { type: 'rackComponent' };

                  parent.add(panel);
                  return panel;
                }

                // 添加透明的可点击区域，提高可用性
                const clickableGeo = new THREE.BoxGeometry(actualWidth, cabinetHeight, actualDepth);
                const clickableMat = new THREE.MeshBasicMaterial({
                  transparent: true,
                  opacity: 0.01, // 几乎不可见
                  side: THREE.DoubleSide,
                  depthWrite: false // 不写入深度缓冲，确保不影响其他渲染
                });
                const clickableMesh = new THREE.Mesh(clickableGeo, clickableMat);
                clickableMesh.position.set(0, 0, 0); // 相对于rackGroup的位置
                clickableMesh.userData = {
                  isCabinet: true, // 重要:添加这个标记使点击检测正常工作
                  cabinetId: cab.id, // 存储机柜ID
                  type: 'rackComponent',
                  isRackCollider: true,
                  parentRackId: cab.id // 存储父级机柜ID
                };
                rackGroup.add(clickableMesh);

                // 删除U位刻度代码，不再渲染U位刻度

                // 添加机柜顶部标签 - 类似图片中的R-1, R-2, R-3样式
                if (showLabels.value) {
                  // 创建标签板
                  const labelWidth = actualWidth * 0.6;
                  const labelHeight = 50;
                  const labelGeo = new THREE.BoxGeometry(labelWidth, labelHeight, 5);
                  const labelMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
                  const labelMesh = new THREE.Mesh(labelGeo, labelMat);

                  // 根据机柜旋转调整标签位置
                  if (isRotated) {
                    // 旋转90度时，前面变成了侧面，所以要用X轴偏移
                    labelMesh.position.set(
                      cab.x + actualDepth / 2,
                      cab.y + cabinetHeight + labelHeight,
                      cab.z
                    );
                    labelMesh.rotation.y = Math.PI / 2;
                  } else {
                    // 不旋转时，正常在前面
                    labelMesh.position.set(
                      cab.x,
                      cab.y + cabinetHeight + labelHeight,
                      cab.z + actualDepth / 2
                    );
                  }

                  scene.add(labelMesh);

                  // 添加文字
                  const cabinetLabel = makeTextSprite(cab.name, {
                    fontsize: 20,
                    textColor: "white",
                    backgroundColor: null
                  });

                  // 根据机柜旋转调整文字位置和方向
                  if (isRotated) {
                    cabinetLabel.position.set(
                      cab.x + actualDepth / 2 + 3,
                      cab.y + cabinetHeight + labelHeight,
                      cab.z
                    );
                    cabinetLabel.rotation.y = Math.PI / 2;
                  } else {
                    cabinetLabel.position.set(
                      cab.x,
                      cab.y + cabinetHeight + labelHeight,
                      cab.z + actualDepth / 2 + 3
                    );
                  }

                  cabinetLabel.userData.isLabel = true;
                  scene.add(cabinetLabel);
                }
              });
            }

            // 检查走线架下方是否有机柜，以在走线架底部渲染开孔
            function checkCabinetBelowCableway(cableway) {
              for (const cab of cabinets.value) {
                const cabinetWidth = cab.width || config.cabinet.defaultWidth;
                const cabinetDepth = cab.depth || config.cabinet.defaultDepth;
                const cabinetHeight = cab.u * config.cabinet.uHeight;
                const cabinetTop = cab.y + cabinetHeight;

                // 走线架高度必须高于机柜顶部
                const cablewayBottom = Math.min(cableway.startPoint.y, cableway.endPoint.y) - config.cableway.defaultHeight / 2;
                if (cablewayBottom < cabinetTop) continue;

                // 检查水平投影是否有重叠
                const cwMinX = Math.min(cableway.startPoint.x, cableway.endPoint.x) - cableway.width / 2;
                const cwMaxX = Math.max(cableway.startPoint.x, cableway.endPoint.x) + cableway.width / 2;
                const cwMinZ = Math.min(cableway.startPoint.z, cableway.endPoint.z) - cableway.width / 2;
                const cwMaxZ = Math.max(cableway.startPoint.z, cableway.endPoint.z) + cableway.width / 2;

                const cabMinX = cab.x - cabinetWidth / 2;
                const cabMaxX = cab.x + cabinetWidth / 2;
                const cabMinZ = cab.z - cabinetDepth / 2;
                const cabMaxZ = cab.z + cabinetDepth / 2;

                // 检查是否有重叠
                if (
                  cwMaxX >= cabMinX && cwMinX <= cabMaxX &&
                  cwMaxZ >= cabMinZ && cwMinZ <= cabMaxZ
                ) {
                  return cab;
                }
              }

              return null;
            }

            // 检查位置是否在某个机柜内
            function findCabinetIdByPosition(pos) {
              for (const cab of cabinets.value) {
                const cabinetWidth = cab.width || config.cabinet.defaultWidth;
                const cabinetDepth = cab.depth || config.cabinet.defaultDepth;
                const cabinetHeight = cab.u * config.cabinet.uHeight;

                // 检查位置是否在机柜范围内
                if (
                  pos.x >= cab.x - cabinetWidth / 2 &&
                  pos.x <= cab.x + cabinetWidth / 2 &&
                  pos.z >= cab.z - cabinetDepth / 2 &&
                  pos.z <= cab.z + cabinetDepth / 2 &&
                  pos.y >= cab.y &&
                  pos.y <= cab.y + cabinetHeight
                ) {
                  return cab.id;
                }
              }
              return null;
            }

            // 数据导入导出功能
            function exportData() {
              // 创建导出对象
              const exportData = {
                cabinets: cabinets.value,
                devices: devices.value,
                patchPanels: patchPanels.value,
                cableways: cableways.value,
                cables: cables.value,
                waypoints: waypoints.value,
                version: '1.1.0'
              };

              // 转换为JSON字符串
              const jsonString = JSON.stringify(exportData, null, 2);

              // 创建Blob对象
              const blob = new Blob([jsonString], { type: 'application/json' });

              // 创建下载链接
              const downloadLink = document.getElementById('download-link');
              downloadLink.href = URL.createObjectURL(blob);
              downloadLink.download = `数据中心布局_${new Date().toISOString().split('T')[0]}.json`;

              // 触发下载
              downloadLink.click();
            }

            function importData() {
              // 触发文件选择对话框
              document.getElementById('import-file').click();
            }

            function handleFileUpload(event) {
              const file = event.target.files[0];
              if (!file) return;

              const reader = new FileReader();
              reader.onload = (e) => {
                try {
                  const data = JSON.parse(e.target.result);

                  // 验证数据有效性
                  if (!data.cabinets || !data.devices || !data.version) {
                    alert('无效的文件格式或数据不完整');
                    return;
                  }

                  // 导入数据
                  cabinets.value = data.cabinets;
                  devices.value = data.devices;
                  patchPanels.value = data.patchPanels || [];
                  cableways.value = data.cableways || [];
                  cables.value = data.cables || [];
                  waypoints.value = data.waypoints || [];

                  // 重新渲染场景
                  renderScene();
                } catch (error) {
                  console.error('导入失败:', error);
                  alert('导入失败，请检查文件格式');
                }
              };

              reader.readAsText(file);

              // 清除文件选择，以便可以重复选择同一文件
              event.target.value = '';
            }



            // 自动检测并创建走线架交叉点
            function detectAndCreateIntersections() {
              for (let i = 0; i < cableways.value.length; i++) {
                for (let j = i + 1; j < cableways.value.length; j++) {
                  const cw1 = cableways.value[i];
                  const cw2 = cableways.value[j];

                  // 检查是否相交
                  if (checkCablewaysIntersect(cw1, cw2)) {
                    // 找到交叉点
                    const intersection = findCablewayIntersection(cw1.id, cw2.id);

                    if (intersection) {
                      // 更新两个走线架的连接点
                      if (!cw1.connectionPoints.some(p => pointsEqual(p, intersection))) {
                        cw1.connectionPoints.push({ ...intersection });
                      }

                      if (!cw2.connectionPoints.some(p => pointsEqual(p, intersection))) {
                        cw2.connectionPoints.push({ ...intersection });
                      }
                    }
                  }
                }
              }
            }

            // 新增waypoints相关表单
            // 在setup函数中添加以下数据和函数
            // Waypoint系统
            const waypoints = ref([]);
            const waypointName = ref('');
            const waypointX = ref(0);
            // Initialize waypointY after defining getDefaultWaypointHeight function
            let waypointY; // Will be initialized after function definition
            const waypointZ = ref(0);

            // 自动计算最高机柜上方0.5m的高度
            function getDefaultWaypointHeight() {
              if (cabinets.value.length === 0) return 2000; // 默认高度2000mm

              // 计算最高机柜的顶部高度
              let maxHeight = 0;
              cabinets.value.forEach(cab => {
                const cabinetHeight = cab.y + (cab.u * config.cabinet.uHeight);
                if (cabinetHeight > maxHeight) {
                  maxHeight = cabinetHeight;
                }
              });

              // 返回最高机柜上方0.5m
              return maxHeight + 500;
            }

            // Now initialize waypointY with the correct default height
            waypointY = ref(getDefaultWaypointHeight());

            // 增强删除Waypoint函数，同时删除相关的走线架
            function deleteWaypoint(id) {
              // 保存当前状态以便撤销
              saveStateToHistory();

              // 查找与此Waypoint相关的所有走线架
              const relatedCableways = cableways.value.filter(cw => {
                // 检查走线架的连接点中是否包含此Waypoint
                return cw.connectionPoints.some(cp => {
                  const waypoint = waypoints.value.find(wp => wp.id === id);
                  if (!waypoint) return false;

                  return Math.abs(cp.x - waypoint.x) < 10 &&
                    Math.abs(cp.y - waypoint.y) < 10 &&
                    Math.abs(cp.z - waypoint.z) < 10;
                });
              });

              // 直接删除相关走线架，不要递归调用deleteCableway，因为它会重复保存历史状态
              const cwIdsToDelete = relatedCableways.map(cw => cw.id);
              cableways.value = cableways.value.filter(cw => !cwIdsToDelete.includes(cw.id));

              // 删除Waypoint
              waypoints.value = waypoints.value.filter(wp => wp.id !== id);
              renderScene();
            }



            // 添加Waypoint时自动更新高度
            function addWaypoint() {
              if (!waypointName.value) {
                alert('请输入Waypoint名称');
                return;
              }

              // 自动计算默认高度
              if (waypointY.value === 0) {
                waypointY.value = getDefaultWaypointHeight();
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              waypoints.value.push({
                id: 'wp-' + Date.now() + Math.random(),
                name: waypointName.value,
                x: waypointX.value,
                y: waypointY.value,
                z: waypointZ.value
              });

              waypointName.value = '';
              renderScene();
            }

            // 修复线缆路径验证 - 更宽松的检查
            function isSegmentInCableway(segment, cableway) {
              // 计算线段的中点
              const midpoint = {
                x: (segment.start.x + segment.end.x) / 2,
                y: (segment.start.y + segment.end.y) / 2,
                z: (segment.start.z + segment.end.z) / 2
              };

              // 简化检查 - 检查线段两端点是否足够接近走线架连接点
              // 如果线段的任一端点是连接点附近，则认为是合法的
              for (const point of cableway.connectionPoints) {
                // 检查开始点
                const startDistance = Math.sqrt(
                  Math.pow(segment.start.x - point.x, 2) +
                  Math.pow(segment.start.y - point.y, 2) +
                  Math.pow(segment.start.z - point.z, 2)
                );

                // 检查结束点
                const endDistance = Math.sqrt(
                  Math.pow(segment.end.x - point.x, 2) +
                  Math.pow(segment.end.y - point.y, 2) +
                  Math.pow(segment.end.z - point.z, 2)
                );

                // 如果任一端点在连接点附近，认为有效
                if (startDistance < 100 || endDistance < 100) {
                  return true;
                }
              }

              // 更宽松地检查是否在走线架内
              // 计算走线架的边界（考虑更大的容差）
              const tolerance = 50; // 增加容差，单位毫米
              const cwMinX = Math.min(cableway.startPoint.x, cableway.endPoint.x) - cableway.width / 2 - tolerance;
              const cwMaxX = Math.max(cableway.startPoint.x, cableway.endPoint.x) + cableway.width / 2 + tolerance;
              const cwMinY = Math.min(cableway.startPoint.y, cableway.endPoint.y) - cableway.height / 2 - tolerance;
              const cwMaxY = Math.max(cableway.startPoint.y, cableway.endPoint.y) + cableway.height / 2 + tolerance;
              const cwMinZ = Math.min(cableway.startPoint.z, cableway.endPoint.z) - cableway.width / 2 - tolerance;
              const cwMaxZ = Math.max(cableway.startPoint.z, cableway.endPoint.z) + cableway.width / 2 + tolerance;

              // 检查中点是否在走线架的容差边界内
              const isWithinBounds =
                midpoint.x >= cwMinX && midpoint.x <= cwMaxX &&
                midpoint.y >= cwMinY && midpoint.y <= cwMaxY &&
                midpoint.z >= cwMinZ && midpoint.z <= cwMaxZ;

              return isWithinBounds;
            }

            // 在两个Waypoint之间自动创建走线架
            function connectWaypoints(wpId1, wpId2) {
              const wp1 = waypoints.value.find(wp => wp.id === wpId1);
              const wp2 = waypoints.value.find(wp => wp.id === wpId2);

              if (!wp1 || !wp2) {
                alert('未找到指定的Waypoint');
                return;
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              // 确定走线架类型
              let type = 'linear';

              // 判断是否需要使用其他类型的走线架
              const dx = Math.abs(wp1.x - wp2.x);
              const dz = Math.abs(wp1.z - wp2.z);

              if (dx > 50 && dz > 50) {
                // 如果x和z方向都有较大差距，使用弯角走线架
                type = 'corner';
              }

              const cwName = `${wp1.name}-to-${wp2.name}`;

              if (type === 'linear') {
                // 直线走线架
                const newCableway = {
                  id: 'cw-' + Date.now() + Math.random(),
                  name: cwName,
                  type: 'linear',
                  startPoint: {
                    x: wp1.x,
                    y: wp1.y,
                    z: wp1.z
                  },
                  endPoint: {
                    x: wp2.x,
                    y: wp2.y,
                    z: wp2.z
                  },
                  width: cablewayWidth.value,
                  height: cablewayHeight.value,
                  connectionPoints: [
                    { x: wp1.x, y: wp1.y, z: wp1.z },
                    { x: wp2.x, y: wp2.y, z: wp2.z }
                  ]
                };

                cableways.value.push(newCableway);
              } else if (type === 'corner') {
                // 弯角走线架 - 创建一个拐角点
                const cornerPoint = {
                  x: wp1.x,
                  y: wp1.y,
                  z: wp2.z
                };

                // 第一段 - wp1到corner
                const cw1 = {
                  id: 'cw-' + Date.now() + Math.random() + '-1',
                  name: `${cwName}-part1`,
                  type: 'linear',
                  startPoint: {
                    x: wp1.x,
                    y: wp1.y,
                    z: wp1.z
                  },
                  endPoint: cornerPoint,
                  width: cablewayWidth.value,
                  height: cablewayHeight.value,
                  connectionPoints: [
                    { x: wp1.x, y: wp1.y, z: wp1.z },
                    { x: cornerPoint.x, y: cornerPoint.y, z: cornerPoint.z }
                  ]
                };

                // 第二段 - corner到wp2
                const cw2 = {
                  id: 'cw-' + Date.now() + Math.random() + '-2',
                  name: `${cwName}-part2`,
                  type: 'linear',
                  startPoint: cornerPoint,
                  endPoint: {
                    x: wp2.x,
                    y: wp2.y,
                    z: wp2.z
                  },
                  width: cablewayWidth.value,
                  height: cablewayHeight.value,
                  connectionPoints: [
                    { x: cornerPoint.x, y: cornerPoint.y, z: cornerPoint.z },
                    { x: wp2.x, y: wp2.y, z: wp2.z }
                  ]
                };

                cableways.value.push(cw1);
                cableways.value.push(cw2);
              }

              // 自动检测并创建交叉点
              detectAndCreateIntersections();
              renderScene();
            }



            // 检查两个集合是否连通
            function areConnected(set1, set2) {
              for (const id of set1) {
                if (set2.has(id)) return true;
              }
              return false;
            }

            // 在setup函数中添加waypointMode和连接相关变量
            const waypointMode = ref('add');
            const waypointFrom = ref('');
            const waypointTo = ref('');

            // 连接选中的走线点
            function connectSelectedWaypoints() {
              if (!waypointFrom.value || !waypointTo.value || waypointFrom.value === waypointTo.value) {
                alert('请选择两个不同的走线点');
                return;
              }

              connectWaypoints(waypointFrom.value, waypointTo.value);

              // 重置选择
              waypointFrom.value = '';
              waypointTo.value = '';
            }

            // 改进线缆渲染，确保线缆沿着走线架内部走
            function renderCables() {
              // 缓存相同路径的线缆，用于实现线缆捆绑效果
              const cablePathMap = new Map();

              // 收集线缆路径信息
              cables.value.forEach(cab => {
                const pathKey = JSON.stringify(cab.path.map(p => [Math.round(p.x), Math.round(p.y), Math.round(p.z)]));
                if (!cablePathMap.has(pathKey)) {
                  cablePathMap.set(pathKey, []);
                }
                cablePathMap.get(pathKey).push(cab);
              });

              // 渲染每组线缆
              cablePathMap.forEach((cablesInBundle, pathKey) => {
                const refCable = cablesInBundle[0]; // 参考线缆
                const cableCount = cablesInBundle.length;

                // 计算线缆的中间点，用于放置长度标签
                let midPoint = null;
                if (refCable.path.length > 0) {
                  // 找到线缆路径中间的点或接近中间的点
                  const midIdx = Math.floor(refCable.path.length / 2);
                  if (refCable.path.length === 1) {
                    midPoint = refCable.path[0];
                  } else if (midIdx < refCable.path.length - 1) {
                    // 取中间段的中点
                    const start = refCable.path[midIdx];
                    const end = refCable.path[midIdx + 1];
                    midPoint = {
                      x: (start.x + end.x) / 2,
                      y: (start.y + end.y) / 2,
                      z: (start.z + end.z) / 2
                    };
                  } else {
                    // 如果中间索引是最后一个点，就取最后一段的中点
                    const start = refCable.path[midIdx - 1];
                    const end = refCable.path[midIdx];
                    midPoint = {
                      x: (start.x + end.x) / 2,
                      y: (start.y + end.y) / 2,
                      z: (start.z + end.z) / 2
                    };
                  }
                }

                // 改进：首先扫描路径并优化点，确保每个走线架段都沿着走线架中轴线
                const optimizedPath = optimizeCablePath(refCable.path);

                // 为每段路径创建直线
                for (let i = 0; i < optimizedPath.length - 1; i++) {
                  const start = optimizedPath[i];
                  const end = optimizedPath[i + 1];

                  const points = [
                    new THREE.Vector3(start.x, start.y, start.z),
                    new THREE.Vector3(end.x, end.y, end.z)
                  ];

                  const geometry = new THREE.BufferGeometry().setFromPoints(points);

                  // 设置线缆颜色 - 根据是否在走线架内区分颜色
                  let color = 0x00ffff; // 默认颜色
                  let lineWidth = 3;

                  // 检查当前段是否在走线架内（除了首尾段）
                  if (i > 0 && i < optimizedPath.length - 2) {
                    // 内部段（走线架内）
                    color = 0x00ffff;
                    lineWidth = 3 + Math.min(cableCount, 5); // 根据线缆数量增加宽度，但不超过8
                  } else {
                    // 首尾段（垂直段）
                    color = 0x00ccff;
                    lineWidth = 2 + Math.min(cableCount, 3); // 根据线缆数量增加宽度，但不超过5
                  }

                  const material = new THREE.LineBasicMaterial({ color: color, linewidth: lineWidth });

                  // 使用管道几何体代替线条，使线缆更加明显
                  if (i > 0 && i < optimizedPath.length - 2) {
                    // 对于走线架内的线缆，使用管道几何体
                    const path = new THREE.CatmullRomCurve3([
                      new THREE.Vector3(start.x, start.y, start.z),
                      new THREE.Vector3(end.x, end.y, end.z)
                    ]);

                    // 根据线缆数量动态调整管道半径
                    const tubeRadius = 5 + Math.min(cableCount * 1.5, 15);

                    const tubeGeometry = new THREE.TubeGeometry(path, 1, tubeRadius, 8, false);

                    // 线缆束颜色随数量变化
                    const cableColors = [
                      0x00ffff,  // 1根 - 青色
                      0x00ccff,  // 2根 - 浅蓝
                      0x0099ff,  // 3根 - 蓝色
                      0x0066ff,  // 4根 - 深蓝
                      0xff6600,  // 5根 - 橙色
                      0xff3300,  // 6根 - 红橙
                      0xff0000   // 7+根 - 红色
                    ];

                    const colorIndex = Math.min(cableCount - 1, cableColors.length - 1);
                    const bundleColor = cableColors[colorIndex];

                    const tubeMaterial = new THREE.MeshLambertMaterial({
                      color: bundleColor,
                      transparent: true,
                      opacity: 0.8
                    });

                    const tube = new THREE.Mesh(tubeGeometry, tubeMaterial);

                    // 存储所有线缆ID，便于点击时识别
                    tube.userData.cableIds = cablesInBundle.map(c => c.id);
                    tube.userData.cableId = refCable.id; // 保持向后兼容
                    tube.userData.cableCount = cableCount;
                    scene.add(tube);
                  } else {
                    // 对于垂直段，仍使用线条
                    const line = new THREE.Line(geometry, material);
                    line.userData.cableIds = cablesInBundle.map(c => c.id);
                    line.userData.cableId = refCable.id; // 保持向后兼容
                    scene.add(line);
                  }
                }

                // 添加线缆束标签
                if (showLabels.value && midPoint) {
                  // 显示线缆束信息和总长度
                  const lengthText = `${cableCount}根线缆 (${refCable.length.toFixed(2)} mm)`;
                  const cableLabel = makeTextSprite(lengthText, {
                    fontsize: 20,
                    textColor: "black",
                    backgroundColor: "rgba(180,255,255,0.8)",
                    borderColor: "rgba(0,150,150,0.8)",
                    borderThickness: 2
                  });

                  // 将标签放置在线缆中点上方一点
                  cableLabel.position.set(midPoint.x, midPoint.y + 30, midPoint.z);
                  cableLabel.userData.isLabel = true;
                  scene.add(cableLabel);

                  // 从中点到设备标签之间的连接线
                  const connectorPoints = [
                    new THREE.Vector3(midPoint.x, midPoint.y, midPoint.z),
                    new THREE.Vector3(midPoint.x, midPoint.y + 30, midPoint.z)
                  ];
                  const connectorGeometry = new THREE.BufferGeometry().setFromPoints(connectorPoints);
                  const connectorMaterial = new THREE.LineBasicMaterial({ color: 0x00cccc, linewidth: 1 });
                  const connectorLine = new THREE.Line(connectorGeometry, connectorMaterial);
                  scene.add(connectorLine);
                }
              });
            }

            // 新增函数：优化线缆路径，确保走线架内的线缆正确走线
            function optimizeCablePath(originalPath) {
              if (originalPath.length <= 2) return originalPath;

              const optimizedPath = [originalPath[0]]; // 起始点不变

              // 检查每个中间点，确保在走线架内的点沿着走线架中轴线
              for (let i = 1; i < originalPath.length - 1; i++) {
                const currentPoint = originalPath[i];
                const nextPoint = originalPath[i + 1];

                // 查找包含当前点的走线架
                const containingCableway = findContainingCableway(currentPoint);

                if (containingCableway) {
                  // 如果点在走线架内，将其对齐到走线架中轴线
                  const alignedPoint = alignPointToCablewayAxis(currentPoint, containingCableway);
                  optimizedPath.push(alignedPoint);
                } else {
                  // 如果点不在任何走线架内（如垂直段），保持原样
                  optimizedPath.push(currentPoint);
                }
              }

              // 终点不变
              optimizedPath.push(originalPath[originalPath.length - 1]);

              return optimizedPath;
            }

            // 查找包含指定点的走线架
            function findContainingCableway(point) {
              for (const cw of cableways.value) {
                // 使用宽松的检查，与isSegmentInCableway相似
                const tolerance = 50; // 增加容差，单位毫米
                const cwMinX = Math.min(cw.startPoint.x, cw.endPoint.x) - cw.width / 2 - tolerance;
                const cwMaxX = Math.max(cw.startPoint.x, cw.endPoint.x) + cw.width / 2 + tolerance;
                const cwMinY = Math.min(cw.startPoint.y, cw.endPoint.y) - cw.height / 2 - tolerance;
                const cwMaxY = Math.max(cw.startPoint.y, cw.endPoint.y) + cw.height / 2 + tolerance;
                const cwMinZ = Math.min(cw.startPoint.z, cw.endPoint.z) - cw.width / 2 - tolerance;
                const cwMaxZ = Math.max(cw.startPoint.z, cw.endPoint.z) + cw.width / 2 + tolerance;

                // 检查点是否在走线架的边界内
                if (point.x >= cwMinX && point.x <= cwMaxX &&
                  point.y >= cwMinY && point.y <= cwMaxY &&
                  point.z >= cwMinZ && point.z <= cwMaxZ) {
                  return cw;
                }
              }

              return null;
            }

            // 高亮显示指定线缆
            function highlightCable(cableId) {
              // 清除场景中所有线缆的高亮状态
              scene.traverse(obj => {
                if (obj.userData && obj.userData.cableId) {
                  if (obj.material) {
                    // 如果是单线缆
                    if (obj.userData.cableId === cableId) {
                      // 高亮选中的线缆
                      obj.material.color.set(0xff0000); // 红色高亮
                      obj.material.opacity = 1.0; // 完全不透明
                      if (obj.material.linewidth) {
                        obj.material.linewidth += 2; // 增加线宽
                      }
                    } else {
                      // 其他线缆恢复默认
                      if (obj.userData.cableIds && obj.userData.cableIds.length > 1) {
                        // 线缆束使用默认颜色
                        const cableCount = obj.userData.cableCount || obj.userData.cableIds.length;
                        const colorIndex = Math.min(cableCount - 1, 6); // 0-6 索引
                        const cableColors = [
                          0x00ffff,  // 1根 - 青色
                          0x00ccff,  // 2根 - 浅蓝
                          0x0099ff,  // 3根 - 蓝色
                          0x0066ff,  // 4根 - 深蓝
                          0xff6600,  // 5根 - 橙色
                          0xff3300,  // 6根 - 红橙
                          0xff0000   // 7+根 - 红色
                        ];
                        obj.material.color.set(cableColors[colorIndex]);
                      } else {
                        // 单根线缆恢复默认青色
                        obj.material.color.set(0x00ffff);
                      }
                      obj.material.opacity = 0.8;
                      // 恢复默认线宽
                      if (obj.material.linewidth && obj.material.linewidth > 2) {
                        obj.material.linewidth -= 2;
                      }
                    }
                  }
                }
              });

              // 重新渲染场景
              renderer.render(scene, camera);
            }

            



            // 添加列管理相关变量
            const columnNaming = ref('A-');
            const columnU = ref(52);
            const columnCabinetCount = ref(3);
            const columnPositionX = ref(0);
            const columnPositionZ = ref(0);
            const columnWidth = ref(600);
            const columnDepth = ref(1400);
            const columnOrientation = ref('horizontal'); // 默认为水平排列

            // 分析数据中的列结构
            function analyzeColumns() {
              if (cabinets.value.length === 0) {
                return {
                  columns: [],
                  rowCount: 0
                };
              }

              // 查找所有不同的z坐标，每个z坐标代表一行（水平排列情况下）
              const zCoordinates = [...new Set(cabinets.value.map(cab => cab.z))].sort((a, b) => a - b);
              const rowCount = zCoordinates.length;

              // 查找所有不同的x坐标，每个x坐标代表一列（水平排列情况下）
              const xCoordinates = [...new Set(cabinets.value.map(cab => cab.x))].sort((a, b) => a - b);

              // 构建列信息 - 支持水平和垂直两种排列
              // 检测列的排列方式：如果同一x坐标上有多个不同z坐标的机柜，则为垂直排列
              const columns = [];

              // 处理水平排列的列（沿X轴）
              xCoordinates.forEach((x, index) => {
                const cabsWithSameX = cabinets.value.filter(cab => cab.x === x);
                if (cabsWithSameX.length > 0) {
                  columns.push({
                    index: index + 1,
                    x: x,
                    orientation: 'horizontal',
                    cabinets: cabsWithSameX.sort((a, b) => a.z - b.z)
                  });
                }
              });

              // 处理垂直排列的列（沿Z轴）
              zCoordinates.forEach((z, index) => {
                const cabsWithSameZ = cabinets.value.filter(cab => cab.z === z);
                const uniqueX = [...new Set(cabsWithSameZ.map(cab => cab.x))];

                uniqueX.forEach((x, xIndex) => {
                  const cabsInVerticalColumn = cabsWithSameZ.filter(cab => cab.x === x);
                  if (cabsInVerticalColumn.length > 1) {
                    // 只有当同一z坐标上有多个机柜时才认为是垂直排列
                    columns.push({
                      index: columns.length + 1,
                      z: z,
                      x: x,
                      orientation: 'vertical',
                      cabinets: cabsInVerticalColumn.sort((a, b) => a.x - b.x)
                    });
                  }
                });
              });

              return {
                columns,
                rowCount,
                zCoordinates,
                xCoordinates
              };
            }

            // 在指定位置添加机柜
            function addColumnAtPosition() {
              // 创建新机柜
              const newCabinets = [];

              // 使用指定的位置
              const newX = columnPositionX.value;
              const newZ = columnPositionZ.value;

              // 根据选择的方向生成多个机柜
              const isHorizontal = columnOrientation.value === 'horizontal';

              for (let i = 0; i < columnCabinetCount.value; i++) {
                // 根据方向计算坐标：水平方向沿X轴，垂直方向沿Z轴
                const xCoord = isHorizontal ? newX + i * columnWidth.value : newX;
                // 垂直排列时使用宽度值(600mm)作为Z轴间距
                const zCoord = isHorizontal ? newZ : newZ + i * columnWidth.value;

                const id = `cab-${Date.now()}-${i}`;
                const name = `${columnNaming.value}${i + 1}`;

                // 添加机柜
                newCabinets.push({
                  id: id,
                  name: name,
                  u: columnU.value,
                  x: xCoord,
                  y: 0,
                  z: zCoord,
                  width: columnWidth.value,
                  depth: columnDepth.value,
                  rotation: isHorizontal ? 0 : 90 // 垂直排列时旋转90度
                });
              }

              // 添加新机柜
              cabinets.value = [...cabinets.value, ...newCabinets];

              // 重新渲染场景
              renderScene();

            }

            // 显示操作通知
            function showNotification(message) {
              const notification = document.getElementById('action-notification');
              if (notification) {
                notification.textContent = message;
                notification.classList.add('show');

                // 2秒后自动隐藏
                setTimeout(() => {
                  notification.classList.remove('show');
                }, 2000);
              }
            }

            // 撤销操作
            function undo() {
              if (currentHistoryIndex.value <= 0) {
                showNotification('没有可撤销的操作');
                return; // 没有可撤销的操作
              }

              isUndoRedoOperation.value = true;
              currentHistoryIndex.value--;
              const previousState = historyStates.value[currentHistoryIndex.value];

              // 恢复之前的状态
              cabinets.value = JSON.parse(previousState.cabinets);
              devices.value = JSON.parse(previousState.devices);
              patchPanels.value = JSON.parse(previousState.patchPanels);
              cableways.value = JSON.parse(previousState.cableways);
              cables.value = JSON.parse(previousState.cables);
              waypoints.value = JSON.parse(previousState.waypoints);

              setTimeout(() => {
                isUndoRedoOperation.value = false;
                renderScene();
                showNotification('已撤销操作');
              }, 0);
            }

            // 恢复操作
            function redo() {
              if (currentHistoryIndex.value >= historyStates.value.length - 1) {
                showNotification('没有可恢复的操作');
                return; // 没有可恢复的操作
              }

              isUndoRedoOperation.value = true;
              currentHistoryIndex.value++;
              const nextState = historyStates.value[currentHistoryIndex.value];

              // 恢复下一个状态
              cabinets.value = JSON.parse(nextState.cabinets);
              devices.value = JSON.parse(nextState.devices);
              patchPanels.value = JSON.parse(nextState.patchPanels);
              cableways.value = JSON.parse(nextState.cableways);
              cables.value = JSON.parse(nextState.cables);
              waypoints.value = JSON.parse(nextState.waypoints);

              setTimeout(() => {
                isUndoRedoOperation.value = false;
                renderScene();
                showNotification('已恢复操作');
              }, 0);
            }

            // 保存当前状态到历史记录
            function saveStateToHistory() {
              if (isUndoRedoOperation.value) return; // 如果是撤销/恢复操作，不保存状态

              // 创建当前状态的快照
              const currentState = {
                cabinets: JSON.stringify(cabinets.value),
                devices: JSON.stringify(devices.value),
                patchPanels: JSON.stringify(patchPanels.value),
                cableways: JSON.stringify(cableways.value),
                cables: JSON.stringify(cables.value),
                waypoints: JSON.stringify(waypoints.value),
                timestamp: Date.now()
              };

              // 如果我们在历史记录中间进行了新的操作，删除后面的历史
              if (currentHistoryIndex.value < historyStates.value.length - 1) {
                historyStates.value = historyStates.value.slice(0, currentHistoryIndex.value + 1);
              }

              // 添加新状态并更新索引
              historyStates.value.push(currentState);
              currentHistoryIndex.value = historyStates.value.length - 1;

              // 限制历史记录的数量，避免内存占用过多
              if (historyStates.value.length > 50) {
                historyStates.value.shift(); // 移除最旧的状态
                currentHistoryIndex.value--;
              }
            }

            // 导出当前配置
            function exportColumnData() {
              if (cabinets.value.length === 0) {
                alert('当前没有机柜数据');
                return;
              }

              // 准备导出数据
              const exportData = {
                cabinets: cabinets.value.map(cab => {
                  return {
                    id: cab.id,
                    name: cab.name,
                    u: cab.u,
                    x: cab.x,
                    y: cab.y,
                    z: cab.z,
                    width: cab.width,
                    depth: cab.depth
                  };
                }),
                metadata: {
                  totalCabinets: cabinets.value.length,
                  exportDate: new Date().toISOString()
                }
              };

              // 转换为JSON字符串
              const jsonString = JSON.stringify(exportData, null, 2);

              // 创建Blob对象
              const blob = new Blob([jsonString], { type: 'application/json' });

              // 创建下载链接
              const downloadLink = document.getElementById('download-link');
              downloadLink.href = URL.createObjectURL(blob);
              downloadLink.download = `机柜列数据_${new Date().toISOString().split('T')[0]}.json`;

              // 触发下载
              downloadLink.click();
            }

            // 编辑设备
            function editDevice(id) {
              const device = devices.value.find(d => d.id === id);
              if (!device) return;

              editingDevice.value = JSON.parse(JSON.stringify(device)); // 深复制
              editingDevicePorts.value = device.ports.length;
            }

            // 保存设备编辑
            function saveDeviceEdit() {
              if (!editingDevice.value) return;

              // 验证设备高度和U位
              if (!editingDevice.value.height || editingDevice.value.height < 1) {
                editingDevice.value.height = 1; // 默认至少1U高
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              const maxU = getCabinetU(editingDevice.value.cabinetId);
              if (editingDevice.value.u + editingDevice.value.height - 1 > maxU) {
                alert(`设备太高，无法放入该机柜。当前机柜只有${maxU}U，设备需要${editingDevice.value.height}U高度。`);
                return;
              }

              // 检查是否与其他设备冲突
              const otherDevices = devices.value.filter(dev =>
                dev.cabinetId === editingDevice.value.cabinetId &&
                dev.id !== editingDevice.value.id
              );

              for (const dev of otherDevices) {
                const devStart = dev.u;
                const devEnd = dev.u + (dev.height || 1) - 1;
                const newDevStart = editingDevice.value.u;
                const newDevEnd = editingDevice.value.u + editingDevice.value.height - 1;

                // 检查是否有重叠
                if (!(newDevEnd < devStart || newDevStart > devEnd)) {
                  alert(`修改后的设备与现有设备 ${dev.name} (U${dev.u}~U${devEnd}) 位置冲突`);
                  return;
                }
              }

              // 查找要编辑的设备索引
              const deviceIndex = devices.value.findIndex(d => d.id === editingDevice.value.id);
              if (deviceIndex === -1) return;

              // 保存设备的端口数量变化
              const oldPortCount = devices.value[deviceIndex].ports.length;
              const newPortCount = editingDevicePorts.value;

              // 更新端口数组
              if (newPortCount !== oldPortCount) {
                // 如果端口数量增加，添加新端口
                if (newPortCount > oldPortCount) {
                  editingDevice.value.ports = [
                    ...devices.value[deviceIndex].ports,
                    ...Array(newPortCount - oldPortCount).fill(0)
                  ];
                }
                // 如果端口数量减少，删除多余的端口，同时删除相关线缆
                else if (newPortCount < oldPortCount) {
                  // 首先标记要移除的端口ID
                  const removedPortIndices = Array.from(
                    { length: oldPortCount - newPortCount },
                    (_, i) => oldPortCount - 1 - i
                  );

                  // 生成要移除的端口ID列表
                  const removedPortIDs = removedPortIndices.map(idx =>
                    `${editingDevice.value.id}-${idx}`
                  );

                  // 移除这些端口相关的线缆
                  cables.value = cables.value.filter(cable =>
                    !removedPortIDs.includes(cable.from) &&
                    !removedPortIDs.includes(cable.to)
                  );

                  // 更新端口数组
                  editingDevice.value.ports = editingDevice.value.ports.slice(0, newPortCount);
                }
              }

              // 更新设备
              devices.value[deviceIndex] = editingDevice.value;

              // 清除编辑状态
              editingDevice.value = null;
              editingDevicePorts.value = 0;

              // 重新渲染场景
              renderScene();
            }

            // 取消设备编辑
            function cancelDeviceEdit() {
              editingDevice.value = null;
              editingDevicePorts.value = 0;
            }

            // 开始编辑机柜名称
            function startEditingCabinetName(cabinetId) {
              const cabinet = cabinets.value.find(cab => cab.id === cabinetId);
              if (!cabinet) return;

              editingCabinetId.value = cabinetId;
              editingCabinetName.value = cabinet.name;

              // 使用Vue的nextTick确保DOM已更新，然后聚焦到输入框
              Vue.nextTick(() => {
                if (document.activeElement.tagName !== 'INPUT') {
                  const inputEl = document.querySelector('input[ref="cabinetNameInput"]');
                  if (inputEl) inputEl.focus();
                }
              });
            }

            // 保存机柜名称
            function saveCabinetName(cabinetId) {
              if (!editingCabinetName.value.trim()) {
                alert('机柜名称不能为空');
                return;
              }

              // 检查名称是否已存在（排除当前正在编辑的）
              const existingCabinet = cabinets.value.find(cab =>
                cab.name === editingCabinetName.value && cab.id !== cabinetId
              );

              if (existingCabinet) {
                alert('已存在同名机柜，请使用其他名称');
                return;
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              // 更新机柜名称
              const cabinetIndex = cabinets.value.findIndex(cab => cab.id === cabinetId);
              if (cabinetIndex !== -1) {
                cabinets.value[cabinetIndex].name = editingCabinetName.value;
              }

              // 清除编辑状态
              editingCabinetId.value = null;
              editingCabinetName.value = '';
            }

            // 添加设备到当前选中的机柜
            function addDeviceToSelectedCabinet() {
              if (!selectedCabinet.value) return;

              // 验证设备名称
              if (!selectedCabinetDeviceName.value.trim()) {
                alert('请输入设备名称');
                return;
              }

              // 检查设备名称是否唯一
              const nameExists = devices.value.some(dev => dev.name === selectedCabinetDeviceName.value.trim());
              if (nameExists) {
                alert(`设备名称 "${selectedCabinetDeviceName.value}" 已存在，请使用唯一的名称`);
                return;
              }

              // 验证设备高度和U位
              if (!selectedCabinetDeviceHeight.value || selectedCabinetDeviceHeight.value < 1) {
                selectedCabinetDeviceHeight.value = 1; // 默认至少1U高
              }

              const maxU = selectedCabinet.value.u;
              if (selectedCabinetDeviceU.value + selectedCabinetDeviceHeight.value - 1 > maxU) {
                alert(`设备太高，无法放入该机柜。当前机柜只有${maxU}U，设备需要${selectedCabinetDeviceHeight.value}U高度。`);
                return;
              }

              // 检查是否与现有设备冲突
              const existingDevices = devices.value.filter(dev => dev.cabinetId === selectedCabinet.value.id);
              for (const dev of existingDevices) {
                const devStart = dev.u;
                const devEnd = dev.u + (dev.height || 1) - 1;
                const newDevStart = selectedCabinetDeviceU.value;
                const newDevEnd = selectedCabinetDeviceU.value + selectedCabinetDeviceHeight.value - 1;

                // 检查是否有重叠
                if (!(newDevEnd < devStart || newDevStart > devEnd)) {
                  alert(`新设备与现有设备 ${dev.name} (U${dev.u}~U${devEnd}) 位置冲突`);
                  return;
                }
              }

              // 保存当前状态以便撤销
              saveStateToHistory();

              const devId = 'dev-' + Date.now() + Math.random();
              devices.value.push({
                id: devId,
                name: selectedCabinetDeviceName.value.trim(),
                cabinetId: selectedCabinet.value.id,
                u: selectedCabinetDeviceU.value,
                height: selectedCabinetDeviceHeight.value,
                ports: Array(selectedCabinetDevicePorts.value).fill(0)
              });
              
              // 清空设备名称以便添加下一个设备
              selectedCabinetDeviceName.value = '';
              
              // 重新渲染场景
              renderScene();
            }

            // 高亮显示选中的机柜 - 直接通过修改机柜组件材质
            function highlightSelectedCabinet(cabinetId) {
              console.log("高亮机柜", cabinetId);
              
              // 重置所有机柜的材质
              scene.traverse(obj => {
                // 检查是否是机柜组件
                if (obj.userData && obj.userData.type === 'rackComponent' && obj.material) {
                  if (obj.material.emissive) {
                    // 关闭发光
                    obj.material.emissive.setHex(0x000000);
                    obj.material.emissiveIntensity = 0;
                    
                    // 恢复原始颜色
                    if (obj.userData.isHighlighted) {
                      obj.userData.isHighlighted = false;
                      if (obj.material.color) {
                        obj.material.color.setHex(0x2a2a2a); // 恢复默认颜色
                      }
                    }
                  }
                }
              });
              
              // 如果没有选中的机柜，只重置材质后返回
              if (!cabinetId) {
                console.log("没有选中的机柜，只重置材质");
                renderer.render(scene, camera);
                return;
              }
              
              console.log("查找机柜组件并高亮");
              
              // 查找属于该机柜的所有组件
              scene.traverse(obj => {
                if (obj.userData && 
                   (obj.userData.cabinetId === cabinetId || obj.userData.parentRackId === cabinetId) && 
                   obj.material && 
                   obj.userData.type === 'rackComponent') {
                  
                  // 修改组件材质为高亮状态
                  if (obj.material.emissive) {
                    console.log("高亮组件", obj);
                    
                    // 增加边框发光效果
                    obj.material.emissive.setHex(0xff7700); // 橙色发光
                    obj.material.emissiveIntensity = 0.5; // 发光强度
                    
                    // 记录高亮状态
                    obj.userData.isHighlighted = true;
                  }
                }
              });
              
              // 渲染场景以显示高亮效果
              renderer.render(scene, camera);
            }

            // 触发CSV文件上传
            function triggerCSVFileUpload() {
              document.getElementById('csv-upload').click();
            }
            
            // 处理设备CSV上传 - 全局上传
            function handleDeviceCSVUpload(event) {
              const file = event.target.files[0];
              if (!file) return;
              
              // 检查是否为CSV文件
              if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) {
                alert('请上传CSV格式的文件');
                event.target.value = '';
                return;
              }
              
              const reader = new FileReader();
              reader.onload = function(e) {
                const csvContent = e.target.result;
                processDeviceCSV(csvContent);
              };
              reader.readAsText(file);
              
              // 清空文件选择，以便下次选择同一文件时也能触发change事件
              event.target.value = '';
            }
            
            // 触发在特定机柜中添加设备的CSV文件上传
            function triggerCSVFileUploadForCabinet() {
              if (!selectedCabinet.value) {
                alert('请先选择一个机柜');
                return;
              }
              document.getElementById('csv-upload-cabinet').click();
            }
            
            // 处理设备CSV上传 - 针对特定机柜
            function handleDeviceCSVUploadForCabinet(event) {
              const file = event.target.files[0];
              if (!file) return;
              
              // 检查是否为CSV文件
              if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) {
                alert('请上传CSV格式的文件');
                event.target.value = '';
                return;
              }
              
              const reader = new FileReader();
              reader.onload = function(e) {
                const csvContent = e.target.result;
                processDeviceCSVForSelectedCabinet(csvContent);
              };
              reader.readAsText(file);
              
              // 清空文件选择，以便下次选择同一文件时也能触发change事件
              event.target.value = '';
            }
            
            // 处理CSV内容 - 全局处理，根据机柜名称添加设备
            function processDeviceCSV(csvContent) {
              // 解析CSV
              const lines = csvContent.trim().split('\n');
              
              // 检查CSV是否为空
              if (lines.length <= 1) {
                alert('CSV文件为空或只有标题行');
                return;
              }
              
              // 获取标题行并验证格式
              const headers = lines[0].split(',').map(h => h.trim().toLowerCase());
              
              // 检查必要的标题是否存在
              const requiredHeaders = ['rack_name', 'device_name', 'upos', 'uheight', 'nports'];
              const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
              
              if (missingHeaders.length > 0) {
                alert(`CSV缺少必要的列: ${missingHeaders.join(', ')}`);
                return;
              }
              
              // 获取每个标题的索引
              const rackNameIndex = headers.indexOf('rack_name');
              const deviceNameIndex = headers.indexOf('device_name');
              const uposIndex = headers.indexOf('upos');
              const uheightIndex = headers.indexOf('uheight');
              const nportsIndex = headers.indexOf('nports');
              
              // 保存当前状态以便撤销
              saveStateToHistory();
              
              // 处理每一行数据
              let successCount = 0;
              let errorCount = 0;
              let errorMessages = [];
              
              for (let i = 1; i < lines.length; i++) {
                const line = lines[i].trim();
                if (!line) continue; // 跳过空行
                
                const values = line.split(',').map(v => v.trim());
                
                // 获取数据
                const rackName = values[rackNameIndex];
                const deviceName = values[deviceNameIndex];
                const upos = parseInt(values[uposIndex]);
                const uheight = parseInt(values[uheightIndex]) || 1; // 默认高度为1U
                const nports = parseInt(values[nportsIndex]) || 0; // 默认端口数为0
                
                // 验证数据
                if (!rackName || !deviceName || isNaN(upos)) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 缺少必要数据或格式不正确`);
                  continue;
                }
                
                // 检查机柜是否存在
                const cabinet = cabinets.value.find(cab => cab.name === rackName);
                if (!cabinet) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 机柜 "${rackName}" 不存在`);
                  continue;
                }
                
                // 检查设备名称是否唯一
                const nameExists = devices.value.some(dev => dev.name === deviceName);
                if (nameExists) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 设备名称 "${deviceName}" 已存在`);
                  continue;
                }
                
                // 验证U位和高度
                if (upos <= 0 || upos > cabinet.u) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: U位 ${upos} 超出机柜范围 (1-${cabinet.u})`);
                  continue;
                }
                
                if (upos + uheight - 1 > cabinet.u) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 设备高度超出机柜范围, 设备需要 ${uheight}U, 从 ${upos}U 开始, 机柜总高 ${cabinet.u}U`);
                  continue;
                }
                
                // 检查是否与现有设备冲突
                const existingDevices = devices.value.filter(dev => dev.cabinetId === cabinet.id);
                let hasConflict = false;
                
                for (const dev of existingDevices) {
                  const devStart = dev.u;
                  const devEnd = dev.u + (dev.height || 1) - 1;
                  const newDevStart = upos;
                  const newDevEnd = upos + uheight - 1;
                  
                  // 检查是否有重叠
                  if (!(newDevEnd < devStart || newDevStart > devEnd)) {
                    errorCount++;
                    errorMessages.push(`行 ${i + 1}: 与现有设备 ${dev.name} (U${dev.u}~U${devEnd}) 位置冲突`);
                    hasConflict = true;
                    break;
                  }
                }
                
                if (hasConflict) continue;
                
                // 添加设备
                const devId = 'dev-' + Date.now() + Math.random();
                devices.value.push({
                  id: devId,
                  name: deviceName,
                  cabinetId: cabinet.id,
                  u: upos,
                  height: uheight,
                  ports: Array(nports).fill(0)
                });
                
                successCount++;
              }
              
              // 显示导入结果
              let resultMessage = `成功导入 ${successCount} 个设备`;
              if (errorCount > 0) {
                resultMessage += `，失败 ${errorCount} 个设备\n\n`;
                resultMessage += errorMessages.slice(0, 5).join('\n');
                if (errorMessages.length > 5) {
                  resultMessage += `\n以及其他 ${errorMessages.length - 5} 个错误`;
                }
              }
              
              alert(resultMessage);
              
              // 重新渲染场景
              renderScene();
            }
            
            // 处理CSV内容 - 针对特定机柜
            function processDeviceCSVForSelectedCabinet(csvContent) {
              if (!selectedCabinet.value) {
                alert('请先选择一个机柜');
                return;
              }
              
              // 解析CSV
              const lines = csvContent.trim().split('\n');
              
              // 检查CSV是否为空
              if (lines.length <= 1) {
                alert('CSV文件为空或只有标题行');
                return;
              }
              
              // 获取标题行并验证格式
              const headers = lines[0].split(',').map(h => h.trim().toLowerCase());
              
              // 对于特定机柜上传，不需要rack_name字段
              const requiredHeaders = ['device_name', 'upos', 'uheight', 'nports'];
              const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
              
              if (missingHeaders.length > 0) {
                alert(`CSV缺少必要的列: ${missingHeaders.join(', ')}`);
                return;
              }
              
              // 获取每个标题的索引
              const deviceNameIndex = headers.indexOf('device_name');
              const uposIndex = headers.indexOf('upos');
              const uheightIndex = headers.indexOf('uheight');
              const nportsIndex = headers.indexOf('nports');
              
              // 保存当前状态以便撤销
              saveStateToHistory();
              
              // 处理每一行数据
              let successCount = 0;
              let errorCount = 0;
              let errorMessages = [];
              const cabinet = selectedCabinet.value;
              
              for (let i = 1; i < lines.length; i++) {
                const line = lines[i].trim();
                if (!line) continue; // 跳过空行
                
                const values = line.split(',').map(v => v.trim());
                
                // 获取数据
                const deviceName = values[deviceNameIndex];
                const upos = parseInt(values[uposIndex]);
                const uheight = parseInt(values[uheightIndex]) || 1; // 默认高度为1U
                const nports = parseInt(values[nportsIndex]) || 0; // 默认端口数为0
                
                // 验证数据
                if (!deviceName || isNaN(upos)) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 缺少必要数据或格式不正确`);
                  continue;
                }
                
                // 检查设备名称是否唯一
                const nameExists = devices.value.some(dev => dev.name === deviceName);
                if (nameExists) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 设备名称 "${deviceName}" 已存在`);
                  continue;
                }
                
                // 验证U位和高度
                if (upos <= 0 || upos > cabinet.u) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: U位 ${upos} 超出机柜范围 (1-${cabinet.u})`);
                  continue;
                }
                
                if (upos + uheight - 1 > cabinet.u) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 设备高度超出机柜范围, 设备需要 ${uheight}U, 从 ${upos}U 开始, 机柜总高 ${cabinet.u}U`);
                  continue;
                }
                
                // 检查是否与现有设备冲突
                const existingDevices = devices.value.filter(dev => dev.cabinetId === cabinet.id);
                let hasConflict = false;
                
                for (const dev of existingDevices) {
                  const devStart = dev.u;
                  const devEnd = dev.u + (dev.height || 1) - 1;
                  const newDevStart = upos;
                  const newDevEnd = upos + uheight - 1;
                  
                  // 检查是否有重叠
                  if (!(newDevEnd < devStart || newDevStart > devEnd)) {
                    errorCount++;
                    errorMessages.push(`行 ${i + 1}: 与现有设备 ${dev.name} (U${dev.u}~U${devEnd}) 位置冲突`);
                    hasConflict = true;
                    break;
                  }
                }
                
                if (hasConflict) continue;
                
                // 添加设备
                const devId = 'dev-' + Date.now() + Math.random();
                devices.value.push({
                  id: devId,
                  name: deviceName,
                  cabinetId: cabinet.id,
                  u: upos,
                  height: uheight,
                  ports: Array(nports).fill(0)
                });
                
                successCount++;
              }
              
              // 显示导入结果
              let resultMessage = `成功导入 ${successCount} 个设备`;
              if (errorCount > 0) {
                resultMessage += `，失败 ${errorCount} 个设备\n\n`;
                resultMessage += errorMessages.slice(0, 5).join('\n');
                if (errorMessages.length > 5) {
                  resultMessage += `\n以及其他 ${errorMessages.length - 5} 个错误`;
                }
              }
              
              alert(resultMessage);
              
              // 重新渲染场景
              renderScene();
            }

            // 触发线缆CSV文件上传
            function triggerCableCSVFileUpload() {
              document.getElementById('cable-csv-upload').click();
            }
            
            // 处理线缆CSV上传
            function handleCableCSVUpload(event) {
              const file = event.target.files[0];
              if (!file) return;
              
              // 检查是否为CSV文件
              if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) {
                alert('请上传CSV格式的文件');
                event.target.value = '';
                return;
              }
              
              const reader = new FileReader();
              reader.onload = function(e) {
                const csvContent = e.target.result;
                processCableCSV(csvContent);
              };
              reader.readAsText(file);
              
              // 清空文件选择，以便下次选择同一文件时也能触发change事件
              event.target.value = '';
            }
            
            // 处理线缆CSV内容
            function processCableCSV(csvContent) {
              // 解析CSV
              const lines = csvContent.trim().split('\n');
              
              // 检查CSV是否为空
              if (lines.length <= 1) {
                alert('CSV文件为空或只有标题行');
                return;
              }
              
              // 获取标题行并验证格式
              const headers = lines[0].split(',').map(h => h.trim().toLowerCase());
              
              // 检查必要的标题是否存在
              const requiredHeaders = ['adev', 'aport', 'zdev', 'zport'];
              const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
              
              if (missingHeaders.length > 0) {
                alert(`CSV缺少必要的列: ${missingHeaders.join(', ')}`);
                return;
              }
              
              // 获取每个标题的索引
              const adevIndex = headers.indexOf('adev');
              const aportIndex = headers.indexOf('aport');
              const zdevIndex = headers.indexOf('zdev');
              const zportIndex = headers.indexOf('zport');
              
              // 保存当前状态以便撤销
              saveStateToHistory();
              
              // 处理每一行数据
              let successCount = 0;
              let errorCount = 0;
              let errorMessages = [];
              
              for (let i = 1; i < lines.length; i++) {
                const line = lines[i].trim();
                if (!line) continue; // 跳过空行
                
                const values = line.split(',').map(v => v.trim());
                
                // 获取数据
                const adevName = values[adevIndex];
                const aportNumber = parseInt(values[aportIndex]);
                const zdevName = values[zdevIndex];
                const zportNumber = parseInt(values[zportIndex]);
                
                // 验证数据
                if (!adevName || !zdevName || isNaN(aportNumber) || isNaN(zportNumber)) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 缺少必要数据或格式不正确`);
                  continue;
                }
                
                // 检查设备是否存在
                const adev = devices.value.find(dev => dev.name === adevName);
                const zdev = devices.value.find(dev => dev.name === zdevName);
                
                if (!adev) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: A端设备 "${adevName}" 不存在`);
                  continue;
                }
                
                if (!zdev) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: Z端设备 "${zdevName}" 不存在`);
                  continue;
                }
                
                // 检查端口号是否有效
                if (aportNumber <= 0 || aportNumber > adev.ports.length) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: A端设备 "${adevName}" 没有端口 ${aportNumber} (总端口数: ${adev.ports.length})`);
                  continue;
                }
                
                if (zportNumber <= 0 || zportNumber > zdev.ports.length) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: Z端设备 "${zdevName}" 没有端口 ${zportNumber} (总端口数: ${zdev.ports.length})`);
                  continue;
                }
                
                // 获取端口ID (注意：端口索引从0开始，而显示的端口号从1开始)
                const aportId = adev.id + '-' + (aportNumber - 1);
                const zportId = zdev.id + '-' + (zportNumber - 1);
                
                // 检查是否已连接
                const alreadyConnected = cables.value.some(
                  cable => (cable.from === aportId && cable.to === zportId) || 
                           (cable.from === zportId && cable.to === aportId)
                );
                
                if (alreadyConnected) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: ${adevName}端口${aportNumber} 和 ${zdevName}端口${zportNumber} 已连接`);
                  continue;
                }
                
                // 创建连接
                try {
                  // 计算路径
                  const result = calculateCablePath(aportId, zportId);
                  
                  // 如果路径为空，表示无法连接
                  if (!result.path || result.path.length === 0) {
                    errorCount++;
                    errorMessages.push(`行 ${i + 1}: ${adevName}端口${aportNumber} 和 ${zdevName}端口${zportNumber} 无法连接，未找到有效路径`);
                    continue;
                  }
                  
                  // 添加线缆
                  cables.value.push({
                    id: 'cable-' + Date.now() + Math.random(),
                    from: aportId,
                    to: zportId,
                    path: result.path,
                    length: result.length
                  });
                  
                  successCount++;
                } catch (error) {
                  errorCount++;
                  errorMessages.push(`行 ${i + 1}: 创建连接时出错: ${error.message}`);
                  continue;
                }
              }
              
              // 显示导入结果
              let resultMessage = `成功导入 ${successCount} 条线缆`;
              if (errorCount > 0) {
                resultMessage += `，失败 ${errorCount} 条线缆\n\n`;
                resultMessage += errorMessages.slice(0, 5).join('\n');
                if (errorMessages.length > 5) {
                  resultMessage += `\n以及其他 ${errorMessages.length - 5} 个错误`;
                }
              }
              
              alert(resultMessage);
              
              // 重新渲染场景
              renderScene();
            }

            // 线缆数据表格 (计算属性)
            const cableTableData = computed(() => {
              return cables.value.map(cable => {
                const fromDetails = getPortDetails(cable.from);
                const toDetails = getPortDetails(cable.to);
                
                // 构建源端口和目的端口名称
                const fromPortName = `${fromDetails.cabinetName}-${fromDetails.deviceName}:端口${fromDetails.portNumber}`;
                const toPortName = `${toDetails.cabinetName}-${toDetails.deviceName}:端口${toDetails.portNumber}`;
                
                // 根据路径点坐标查找对应的waypoint
                const pathWaypoints = [];
                if (cable.path && cable.path.length > 0) {
                  cable.path.forEach(point => {
                    // 查找与该点接近的waypoint
                    const nearWaypoint = waypoints.value.find(wp => {
                      const distance = Math.sqrt(
                        Math.pow(wp.x - point.x, 2) + 
                        Math.pow(wp.y - point.y, 2) + 
                        Math.pow(wp.z - point.z, 2)
                      );
                      return distance < 10; // 距离小于10个单位
                    });
                    
                    if (nearWaypoint) {
                      pathWaypoints.push(nearWaypoint.name);
                    } else {
                      pathWaypoints.push(`(${point.x.toFixed(1)}, ${point.y.toFixed(1)}, ${point.z.toFixed(1)})`);
                    }
                  });
                }
                
                // 构建完整路径: 起始端口 → 走线点 → 目的端口
                const fullPathWaypoints = [fromPortName, ...pathWaypoints, toPortName];
                
                return {
                  id: cable.id,
                  fromDevice: `${fromDetails.cabinetName}-${fromDetails.deviceName}`,
                  fromPort: `端口${fromDetails.portNumber}`,
                  toDevice: `${toDetails.cabinetName}-${toDetails.deviceName}`,
                  toPort: `端口${toDetails.portNumber}`,
                  length: cable.length || 0,
                  pathText: cable.path ? cable.path.map(p => `(${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})`).join(' → ') : '无路径',
                  waypointPath: fullPathWaypoints.join(' → ')
                };
              });
            });
            
            // 显示/隐藏线缆列表模态框
            const showCableListModal = ref(false);
            
            // 线缆长度统计 (计算属性)
            const cableStatistics = computed(() => {
              const statistics = {};
              cables.value.forEach(cable => {
                // 将毫米转换为米
                const lengthInM = cable.length / 1000;
                
                // 1. 先添加固定冗余
                const lengthWithFixedRedundancy = lengthInM + fixedRedundancy.value;
                
                // 2. 再添加冗余百分比
                const lengthWithRedundancy = lengthWithFixedRedundancy * (1 + redundancyPercentage.value / 100);
                
                // 计算线缆规格 (向上取整到指定间隔的倍数)
                const interval = lengthInterval.value || 5; // 默认使用5m作为间隔
                const requiredLength = Math.ceil(lengthWithRedundancy / interval) * interval;
                const lengthKey = `${requiredLength}m`;
                
                if (!statistics[lengthKey]) {
                  statistics[lengthKey] = 0;
                }
                statistics[lengthKey]++;
              });
              
              // 按照长度排序
              return Object.fromEntries(
                Object.entries(statistics).sort((a, b) => {
                  return parseInt(a[0]) - parseInt(b[0]);
                })
              );
            });
            
            // 导出CSV报告
            function exportReportCSV() {
              // 准备CSV头部
              let csvContent = "数据来源,源设备,源端口,目的设备,目的端口,长度(mm),完整路径\n";
              
              // 添加线缆数据
              cableTableData.value.forEach(cable => {
                csvContent += `线缆,${cable.fromDevice},${cable.fromPort},${cable.toDevice},${cable.toPort},${cable.length.toFixed(2)},"${cable.waypointPath}"\n`;
              });
              
              // 添加空行分隔
              csvContent += "\n\n";
              
              // 添加统计数据
              csvContent += `线缆长度统计 (${lengthInterval.value}m间隔，含${fixedRedundancy.value}m固定冗余 + ${redundancyPercentage.value}%比例冗余)\n`;
              csvContent += "长度规格,需要数量\n";
              
              Object.entries(cableStatistics.value).forEach(([length, count]) => {
                csvContent += `${length},${count}\n`;
              });
              
              // 创建并下载CSV文件
              const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
              const url = URL.createObjectURL(blob);
              const a = document.createElement('a');
              a.href = url;
              a.download = '线缆统计报告.csv';
              a.click();
              URL.revokeObjectURL(url);
            }

                         // 新增UI控制相关变量
                           const redundancyPercentage = ref(10); // 默认10%冗余
              const lengthInterval = ref(5);      // 默认5m间隔
              const fixedRedundancy = ref(0.5);   // 默认0.5m固定冗余

                return {
              cabinets, devices, patchPanels, cableways, cables, waypoints,
              deviceCabinet, deviceU, deviceHeight, devicePorts,
              patchPanelHeight, patchPanelWidth, patchPanelDepth, patchPanelType, patchPanelCabinet,
              cableFrom, cableTo,
              cablewayMode, cablewayType, cablewayName, cablewayStartX, cablewayStartY, cablewayStartZ, cablewayEndX, cablewayEndY, cablewayEndZ, cablewayWidth, cablewayHeight,
              waypointName, waypointX, waypointY, waypointZ, waypointMode, waypointFrom, waypointTo,
              addDevice, addPatchPanel, addCable, addCableway,
              deleteCabinet, deleteDevice, deleteCable, deletePatchPanel, deleteCableway,
              allPorts, getCabinetU, getCabinetName, selectedCable, getPortLabel, getCablewayTypeLabel,
              showLabels, showConnectors, showCableways,
              getPortDetails, // Return the new helper function
              exportData, importData, handleFileUpload,
              panCamera,
              addWaypoint, deleteWaypoint, connectWaypoints, connectSelectedWaypoints,
              columnNaming, columnU, columnCabinetCount, columnPositionX, columnPositionZ,
              columnWidth, columnDepth, columnOrientation, addColumnAtPosition,
              // 新增UI控制相关变量
              activeMainTab, activeSection, toggleSection,
              // 编辑相关
              editingDevice, editingDevicePorts, editDevice, saveDeviceEdit, cancelDeviceEdit,
              editingCabinetId, editingCabinetName, startEditingCabinetName, saveCabinetName,
              selectedCabinet, updateSelectedCabinet, deleteCabinetAndClose,
              // 撤销和恢复
              undo, redo, historyStates, currentHistoryIndex,
              selectedCabinetDeviceU, selectedCabinetDeviceHeight, selectedCabinetDevicePorts, selectedCabinetDeviceName,
              addDeviceToSelectedCabinet,
              // CSV上传相关
              triggerCSVFileUpload, handleDeviceCSVUpload, triggerCSVFileUploadForCabinet, handleDeviceCSVUploadForCabinet,
              // 线缆CSV上传相关
              triggerCableCSVFileUpload, handleCableCSVUpload,
              // Report related data and methods
              cableTableData, cableStatistics, exportReportCSV, showCableListModal,
              redundancyPercentage, lengthInterval, fixedRedundancy
            }
          }
        }).mount('#app');
      </script>
</body>

</html>